diff --git a/.gitattributes b/.gitattributes index 12067c59..8f7af5d4 100644 --- a/.gitattributes +++ b/.gitattributes @@ -12,4 +12,23 @@ # stay as plain git blobs: Go modules resolve through the module proxy (no LFS # smudge), and the Kotlin/Python dylibs are small build inputs for the # Maven/PyPI artifacts. +# +# The C carrier's dynamic libraries (staged by tooling/connect/cpp-build.mjs, +# Windows refresh via the `windows-smoke` lane of +# .github/workflows/cpp-connect.yml) are ~6 MB each and are tracked the same +# way. Its import library (`*.dll.lib`, ~150 KB) and `provenance.json` stay +# plain git blobs: they are small, and every checkout — including the lanes +# that only hash or diff the record — has to read them without an LFS fetch. +# C/C++ consumers clone the repo, so they need git-lfs (`git lfs install` +# once, `git lfs pull` in an existing clone). crates/spoke-connect/bindings/swift/xcframework/**/*.a filter=lfs diff=lfs merge=lfs -text +crates/spoke-connect/bindings/cpp/native/**/*.dll filter=lfs diff=lfs merge=lfs -text +crates/spoke-connect/bindings/cpp/native/**/*.dylib filter=lfs diff=lfs merge=lfs -text + +# The C contract header's bytes are part of the published C carrier record: +# `tooling/connect/cpp-build.mjs` hashes it into `headerSha256` in +# bindings/cpp/native/provenance.json, and consumers verify their copy against +# that hash. `eol=lf` keeps the checked-out bytes identical on every platform — +# without it a Windows `core.autocrlf=true` checkout hands the build CRLF bytes +# and records a hash no LF checkout can ever match. +crates/spoke-connect/bindings/cpp/include/spoke_connect.h text eol=lf diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3efa18da..ba557f69 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,12 +1,11 @@ name: CI on: + # Unfiltered on pull requests: verify-codegen / typescript / rust / + # verify-version / connect-identity / golden-vector-sync are required checks, + # and a required context a path filter skips can never be satisfied. The + # docs-only optimization survives on the push side below. pull_request: - # Skip the compile/test matrix for docs-only changes; docs-site validation lives in docs.yml. - paths-ignore: - - 'docs/**' - - 'tooling/docs/**' - - '**/*.md' push: branches: - main @@ -125,9 +124,6 @@ jobs: - name: Checkout # actions/checkout@v4.2.2 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 - with: - # Full history so the path-filter diff below can compare base vs head. - fetch-depth: 0 - name: Setup Node.js # actions/setup-node@v4.4.0 — Node 24 matches the release.yml npm publish surface @@ -135,40 +131,11 @@ jobs: with: node-version: 24 - - name: Check identity-related paths changed - id: paths - run: | - set -euo pipefail - if [[ "$GITHUB_EVENT_NAME" == "pull_request" ]]; then - base="${{ github.event.pull_request.base.sha }}" - head="${{ github.event.pull_request.head.sha }}" - else - base="${{ github.event.before }}" - head="${{ github.event.after }}" - fi - # First push to a branch has an all-zero `before` sha — diff against - # the empty tree so the gate still runs. - if [[ "$base" =~ ^0+$ ]]; then - base="$(git hash-object -t tree /dev/null)" - fi - # Fail open on diff errors: if git diff cannot resolve the base/head - # shas (e.g. force-push GC'd the old commit), run the proof anyway — - # a regression gate that silently skips on its own filter error is - # a vacuous pass. The warning makes the fallback visible. - if ! changed="$(git diff --name-only "$base" "$head" 2>/dev/null)"; then - echo "::warning::git diff failed for $base..$head; running identity proof anyway" - echo "run=true" >> "$GITHUB_OUTPUT" - else - matches="$(printf '%s\n' "$changed" | grep -Ec '^(tooling/connect-identity-proof/|packages/spoke-connect-ts/|\.github/workflows/ci\.yml)' || true)" - if [[ "${matches:-0}" -gt 0 ]]; then - echo "run=true" >> "$GITHUB_OUTPUT" - else - echo "run=false" >> "$GITHUB_OUTPUT" - fi - fi - - name: Run connect identity proof - if: steps.paths.outputs.run == 'true' + # A required PR check runs on every pull request: a changed-path gate + # would report this context green without proving anything, and an + # unscheduled-proof pass is indistinguishable from a real one. The + # proof is zero-dependency, so the minute it costs is the honest price. run: node tooling/connect-identity-proof/proof.mjs rust: diff --git a/.github/workflows/cpp-connect.yml b/.github/workflows/cpp-connect.yml index 606c38bf..35a74a93 100644 --- a/.github/workflows/cpp-connect.yml +++ b/.github/workflows/cpp-connect.yml @@ -4,20 +4,10 @@ name: C++ connect (Windows + macOS) # refresh artifact; macOS validates the committed osx-arm64 carrier, then both # lanes prove exported symbols and run the shared C++ smoke. on: + # Unfiltered on pull requests: windows-smoke / macos-smoke are required + # checks, so every PR schedules both carrier lanes. The push filter below is + # unchanged, so pushes outside those paths still skip them. pull_request: - paths: - - Cargo.toml - - Cargo.lock - - crates/spoke-connect-capi/** - - crates/spoke-connect/src/** - - crates/spoke-connect/Cargo.toml - - crates/spoke-connect/bindings/cpp/include/** - - crates/spoke-connect/bindings/cpp/Smoke/** - - crates/spoke-connect/bindings/cpp/native/** - - tooling/connect/cpp-build.mjs - - tooling/connect/cpp-symbol-check.mjs - - tooling/connect/cpp-smoke.mjs - - .github/workflows/cpp-connect.yml push: branches: - main @@ -56,8 +46,11 @@ jobs: CARGO_ENCODED_RUSTFLAGS: "" steps: - name: Checkout - # actions/checkout@v4.2.2 + # actions/checkout@v4.2.2 — lfs: true so the symbol check, the smoke and + # the provenance check hash the real carrier, not an LFS pointer. uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + with: + lfs: true - name: Setup MSVC developer environment # x64 selects the release MSVC toolchain used by the C++ smoke. @@ -101,6 +94,17 @@ jobs: throw "cl.exe version probe failed with exit code $LASTEXITCODE" } + - name: Verify committed provenance + # Reports drift without cancelling the lane: the build, the smoke and + # the uploaded refresh artifact below still run, so a stale record can + # be replaced by a real rebuild instead of being hand-edited. The final + # `Fail on provenance drift` step re-fails the job from this step's + # *outcome* (continue-on-error leaves the outcome `failure`), so the + # lane can never end green while the committed bytes disagree. + id: provenance + continue-on-error: true + run: node tooling/connect/cpp-build.mjs --verify --target x86_64-pc-windows-msvc + - name: Build Windows carrier run: node tooling/connect/cpp-build.mjs --target x86_64-pc-windows-msvc --toolchain 1.96.0 @@ -131,6 +135,18 @@ jobs: if-no-files-found: error retention-days: 5 + - name: Fail on provenance drift + # The lane's verdict. `continue-on-error` above keeps the refresh + # artifact flowing, but the job still has to go red when the committed + # record does not describe the committed bytes — it reads the raw step + # outcome, so drift is never absorbed into a green run. + if: always() + shell: pwsh + run: | + if ("${{ steps.provenance.outcome }}" -ne "success") { + throw "committed provenance does not describe the committed win-x64 bytes; apply the uploaded refresh artifact (a genuine Windows rebuild) before this lane can pass" + } + macos-smoke: name: macos-smoke runs-on: macos-14 @@ -142,8 +158,11 @@ jobs: CARGO_ENCODED_RUSTFLAGS: "" steps: - name: Checkout - # actions/checkout@v4.2.2 + # actions/checkout@v4.2.2 — lfs: true so the committed carrier arrives as + # bytes; the provenance check and the symbol check read it. uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + with: + lfs: true - name: Setup Node.js # actions/setup-node@v4.4.0 — Node 24 @@ -179,6 +198,11 @@ jobs: clang++ --version rustc +1.96.0 -Vv + - name: Verify committed provenance + # Fails hard: this lane produces no artifact, so nothing downstream has + # to run for the drift to be actionable. + run: node tooling/connect/cpp-build.mjs --verify --target aarch64-apple-darwin + - name: Show native provenance shell: bash run: cat crates/spoke-connect/bindings/cpp/native/provenance.json diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 74ba9619..6b7922e6 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -1,11 +1,12 @@ name: Docs site # Documentation gate: validates every documentation change — the site under -# docs/ (with its tooling) and any Markdown file elsewhere. `ci.yml` skips its -# compile/test matrix for documentation-only change sets, so this workflow is -# the validation entry point; it fails if `pnpm docs:build` breaks, if an EN -# page ships without its CN twin (twin-parity), or if an internal link resolves -# to a missing page (dead-link). +# docs/ (with its tooling) and any Markdown file elsewhere. It fails if +# `pnpm docs:build` breaks, if an EN page ships without its CN twin +# (twin-parity), or if an internal link resolves to a missing page +# (dead-link). `Build docs site` is a required check, so the pull-request +# trigger is unfiltered — a path filter would leave that context unscheduled +# and unmergeable on non-docs PRs; the push side keeps the docs-only filter. # Deploy: on push to main, the built site is published to GitHub Pages via the # official Pages actions (upload-pages-artifact + deploy-pages). # @@ -22,13 +23,6 @@ name: Docs site on: pull_request: - paths: - - docs/** - - tooling/docs/** - - '**/*.md' - - .github/workflows/docs.yml - - package.json - - pnpm-lock.yaml push: branches: - main diff --git a/.github/workflows/xcframework.yml b/.github/workflows/xcframework.yml index 11586403..c7b8dbe3 100644 --- a/.github/workflows/xcframework.yml +++ b/.github/workflows/xcframework.yml @@ -2,9 +2,9 @@ name: Xcframework # Dedicated xcframework job: assembles the three-slice spoke_connectFFI # xcframework from the checkout's Rust sources and fails the job when the -# built bytes drift from the committed (LFS) artifact. Path-filtered so the -# macOS runner is used only when the FFI surface (or this automation itself) -# changes — never per-push unconditional. Mirrors ci.yml's event set. +# built bytes drift from the committed (LFS) artifact. `xcframework` is a +# required check, so every pull request runs it and pays the macOS runner; the +# push side keeps the FFI-surface filter, and ci.yml mirrors that event set. # # Deliberately off-filter: crates/spoke-operations/**, crates/spoke-schemas/**, # crates/spoke-fixture-toy-world/** — they change artifact bytes but not the @@ -12,15 +12,6 @@ name: Xcframework # verify-binding-regen.sh gate (generated Swift tree diff). on: pull_request: - paths: - - crates/spoke-connect/src/** - - crates/spoke-connect/uniffi-bindgen.rs - - crates/spoke-connect/Cargo.toml - - Cargo.lock - - tooling/connect/build-swift-xcframework.sh - - tooling/connect/verify-xcframework-drift.sh - - crates/spoke-connect/bindings/swift/generated/** - - .github/workflows/xcframework.yml push: branches: - main diff --git a/.mstar/knowledge/README.md b/.mstar/knowledge/README.md index 72a0c42f..b7efe6da 100644 --- a/.mstar/knowledge/README.md +++ b/.mstar/knowledge/README.md @@ -39,7 +39,7 @@ Durable protocol and tooling facts. Normative specs live in [`specs/`](../specs/ | [`architecture-patterns/connect-golden-vector-ssot.md`](architecture-patterns/connect-golden-vector-ssot.md) | architecture-patterns | compound 2026-08-04 (golden vector) | Cross-language golden vector SSOT pattern: single shared JSON artifact (inputs + pinned outputs) consumed by every language; registered byte-identical copies for bindings that cannot read monorepo-relative paths; sync gate covering ALL copies (not just Rust↔TS); test-only boundary (`#[cfg(test)]` / non-exported) | | [`conventions/docs-i18n-glossary.md`](conventions/docs-i18n-glossary.md) | conventions | compound 2026-08-03 | CN integrator-docs terminology glossary: authority order (README_CN → CONCEPTS → glossary); wire-term EN+gloss table; VitePress zh nav labels; SSOT stays English; twin consistency rules | | [`architecture-patterns/connect-capability-token-auth.md`](architecture-patterns/connect-capability-token-auth.md) | architecture-patterns | compound 2026-08-01 (capability-token); 2026-08-04 issuance ergonomics + canonical-sig rule | capability-token auth method: claims/proof design (deny-unknown-fields, sig over JCS(claims)); trusted_issuers trust root; sub/aud/exp checks; membership capability matching; challenge/response + per-invoke auth; `auth_failed` vocabulary; base58btc inverse peer-id decode; offline validation, jti reserved for revocation; **issuance-side fail-fast** (empty caps / exp / iat guards with required 3rd positional `now: u64` on Rust `issue_capability_token`); **canonical base64url signature rule** (RFC 4648 round-trip check on both sides; see `base64-canonical-sig-ts-rust-parity.md`) | -| [`testing-patterns/connect-identity-parity-proof.md`](testing-patterns/connect-identity-parity-proof.md) | testing-patterns | compound 2026-08-01 (identity proof); 2026-08-04 CI gate | Cross-language identity-byte parity: golden vectors as contract; standalone proof-script pattern (`tooling/connect-identity-proof/`), CI-gated via the `connect-identity` job (Node 24, fail-open path filter); port gotchas (protobuf PublicKey, identity multihash, JCS optional-field omission, base64url, WebCrypto DER) | +| [`testing-patterns/connect-identity-parity-proof.md`](testing-patterns/connect-identity-parity-proof.md) | testing-patterns | compound 2026-08-01 (identity proof); 2026-08-04 CI gate; 2026-09-22 required-check gate | Cross-language identity-byte parity: golden vectors as contract; standalone proof-script pattern (`tooling/connect-identity-proof/`), CI-gated via the `connect-identity` job (Node 24, every pull request — a required check, not path-filtered); port gotchas (protobuf PublicKey, identity multihash, JCS optional-field omission, base64url, WebCrypto DER) | | [`tooling-decisions/connect-publish-staging.md`](tooling-decisions/connect-publish-staging.md) | tooling-decisions | compound 2026-08-03 (publish strategy) | Registry split: npm/crates.io for primary TS/Rust connect; binding channels grouped registry-backed (GitHub Packages NuGet/Maven, PyPI OIDC) vs git-based (SPM, Go modules, committed C/C++ headers and platform carriers); lockstep SemVer on the same tag gate | | [`architecture-patterns/integrator-docs-site-ssot-links.md`](architecture-patterns/integrator-docs-site-ssot-links.md) | architecture-patterns | compound 2026-08-02 (docs site); 2026-08-04 Diátaxis four-quadrant + zero-spec-links restructure; 2026-09-18 (anchor/link discipline) | Integrator docs-site pattern: VitePress `docs/` with **Diátaxis four-quadrant layout** (Tutorials / How-to / Reference / Explanation); **audience boundary — zero `.mstar/specs/` links on integrator pages** (consolidated Reference carries field tables inline; external canonicals + root CONCEPTS.md for vocabulary); GitHub Pages deploy via SHA-pinned Actions; concurrency lesson (ref-scoped workflow group so PR runs never cancel main deploys); EN↔CN twin-parity CI gate; **wire-fact fidelity** (5 at-risk fact categories to land explicitly during consolidation); **anchor/link discipline** (cross-page fragments must match the emitted slug — em dash kept, digit-led headings get `_`; each locale links its own slugs) with the twin-parity → build → deadlink chain as the docs-change evidence; one owning page per fact with sibling cross-links | | [`architecture-patterns/base64-canonical-sig-ts-rust-parity.md`](architecture-patterns/base64-canonical-sig-ts-rust-parity.md) | architecture-patterns | compound 2026-08-04 (canonical-sig round) | Canonical base64url signature round-trip across TS (slack-lenient `atob`) and Rust (`base64` 0.22 strict `URL_SAFE_NO_PAD`): keep the round-trip check on both sides even when one side makes it unreachable — source-level parity invariant + defense-in-depth against decoder relaxation; slack-bit mutation formula for the cross-language test fixture; new-language-port checklist (verify decoder strictness first) | @@ -51,7 +51,7 @@ Durable protocol and tooling facts. Normative specs live in [`specs/`](../specs/ | [`architecture-patterns/connect-ts-responder-from-public-exports.md`](architecture-patterns/connect-ts-responder-from-public-exports.md) | architecture-patterns | compound 2026-08-06 (connect-demo-mvp) | TS responder over public `@42ch/spoke-connect` exports: v2 envelope-auth helpers module-internal → reimplement 3 algorithm ids (locked field sets) + pinned canonicalize@3.0.0; responder-phase recipe mirroring loopback-host (allowlist-first → hello verify → nonce → dial-bound responder hello → signed session snapshot; gate = peek → verify → advance); `port.*`→`spoke-baseline` dispatch product map; strict-client e2e proof = bidirectional signature interop | | [`architecture-patterns/reverse-invoke-on-symmetric-wire.md`](architecture-patterns/reverse-invoke-on-symmetric-wire.md) | architecture-patterns | compound 2026-08-16 (reverse tools capability) | Reverse invokes over a symmetric request/response wire: request-shape-first classification (op-bearing docs are never responses), auth-before-advance serving order, lock-before-allocate wire-order invariant (+ YieldingSendTransport ordering-test template with red/green discipline), self-describing `tools.*` capability model (no umbrella flag, ns ownership) | | [`tooling-decisions/idempotent-registry-publish.md`](tooling-decisions/idempotent-registry-publish.md) | tooling-decisions | compound 2026-08-18 (release re-run safety) | Registry publish idempotency: pre-check primary + per-file skip guard + unconditional confirm re-probe as sole arbiter (`continue-on-error` on duplicate-failing publishers); fail loud on doubt; full-set skip, partial never silent-green; PyPI yanked=absent + PEP 440 filename normalization; Maven pom+module+jar probe with JNA jar-entries check | -| [`tooling-decisions/ci-assembled-committed-native-artifacts.md`](tooling-decisions/ci-assembled-committed-native-artifacts.md) | tooling-decisions | compound 2026-08-18 (xcframework CI automation); 2026-08-18 FFI-ABI refresh round; 2026-09-18 dispatch-ref prerequisite | Committed native artifacts assembled in CI: path-filtered build + committed-artifact SHA-256 drift baseline + fail-closed apply script (mandatory manifest checksum, run provenance, git-lfs guard); no auto-commit (LFS × token × CI-retrigger); Info.plist ordering normalization + bash 3.2 empty-array idiom; DEFAULT-suite smokes consume committed natives (ABI change ⇒ rebuild+commit before suites pass); branch-independent refresh via dispatch-only `workflow_dispatch` (apply → drift-red → commit → same-SHA drift-green) | +| [`tooling-decisions/ci-assembled-committed-native-artifacts.md`](tooling-decisions/ci-assembled-committed-native-artifacts.md) | tooling-decisions | compound 2026-08-18 (xcframework CI automation); 2026-08-18 FFI-ABI refresh round; 2026-09-18 dispatch-ref prerequisite; 2026-09-22 required-check triggers | Committed native artifacts assembled in CI: required-check build (every pull request; the push side stays path-filtered on the FFI surface) + committed-artifact SHA-256 drift baseline + fail-closed apply script (mandatory manifest checksum, run provenance, git-lfs guard); no auto-commit (LFS × token × CI-retrigger); Info.plist ordering normalization + bash 3.2 empty-array idiom; DEFAULT-suite smokes consume committed natives (ABI change ⇒ rebuild+commit before suites pass); branch-independent refresh via dispatch-only `workflow_dispatch` (apply → drift-red → commit → same-SHA drift-green) | | [`testing-patterns/falsify-error-classification-before-surgical-fixes.md`](testing-patterns/falsify-error-classification-before-surgical-fixes.md) | testing-patterns | compound 2026-08-18 (ci:typescript root cause) | Before planning fixes from a multi-error inventory, falsify the classification with an artifact A/B (dependency `dist/` present vs removed); `unknown`/`any`-adjacent errors downstream of a resolution failure are collapse symptoms, not genuine defects | | [`architecture-patterns/ke-capability-flag-naming.md`](architecture-patterns/ke-capability-flag-naming.md) | architecture-patterns | compound 2026-09-17 (ke axes) | Capability flags anchor to the concern's actual carrier: `l-` only for genuine single-layer concerns, named `ke-` family for cross-layer ones; independent flags, no umbrella; viewpoint on shared `Scope`; owner references holder KE `entry_id` | | [`architecture-patterns/extract-op-injected-extraction.md`](architecture-patterns/extract-op-injected-extraction.md) | architecture-patterns | compound 2026-09-17 (ke axes + remote/FFI exposure) | `ke-extraction` extract op: standalone `ExtractionPort` + `orchestrateExtract` with injected async extractor (never composed into BaselinePorts); correlation `run_id` without durable run objects; provisional-only candidates through unchanged promote gates; null/absent `coverage_hint` equivalence across TS/Rust; **remote/FFI exposure**: service-shaped op over connect (loader never on wire), explicit service composition so a blanket probe cannot mask it, three distinguishable unavailability paths, paired-observation parity proof, and the host-side `Send` bridge constraint | diff --git a/.mstar/knowledge/architecture-patterns/integrator-docs-site-ssot-links.md b/.mstar/knowledge/architecture-patterns/integrator-docs-site-ssot-links.md index 4d3a26e4..355f267b 100644 --- a/.mstar/knowledge/architecture-patterns/integrator-docs-site-ssot-links.md +++ b/.mstar/knowledge/architecture-patterns/integrator-docs-site-ssot-links.md @@ -1,7 +1,7 @@ --- module: spoke-docs date: 2026-08-02 -last_updated: 2026-09-18 +last_updated: 2026-09-22 problem_type: architecture_pattern category: architecture-patterns severity: medium @@ -116,7 +116,7 @@ An integrator site for a protocol repo must be a **window onto the specs, not a - Building consumer-facing docs for any protocol repository — integrator summaries + GitHub blob SSOT links; specs read-only; no body duplication. - Deploying a VitePress site to GitHub Pages from Actions — SHA-pinned Pages actions, Pages source "GitHub Actions", ref-scoped build concurrency + main-only deploy group, `base: '/'` when a custom domain is attached; otherwise `base: '//'` for bare project sites. -- Locale twin sites (EN root + `zh/`) — a zero-dep twin-parity script (`tooling/docs/twin-parity.mjs`) gates page-count drift: every `*.md` under the root locale has a path-identical twin under `zh/` (and vice versa), with an explicit allow-list for justified locale-specific pages. A dead-link script (`tooling/docs/deadlink-check.mjs`) crawls the built `dist/**/*.html` for internal `href`s under the site base and fails on any 404. Both run in the `build` job before `upload-pages-artifact`, on the same `docs/**` + `tooling/docs/**` path filter. +- Locale twin sites (EN root + `zh/`) — a zero-dep twin-parity script (`tooling/docs/twin-parity.mjs`) gates page-count drift: every `*.md` under the root locale has a path-identical twin under `zh/` (and vice versa), with an explicit allow-list for justified locale-specific pages. A dead-link script (`tooling/docs/deadlink-check.mjs`) crawls the built `dist/**/*.html` for internal `href`s under the site base and fails on any 404. Both run in the `build` job before `upload-pages-artifact`; the push side is filtered to `docs/**` + `tooling/docs/**`, while pull requests run the gate unfiltered because `Build docs site` is a required check (a filtered-out required context can never be satisfied). ## Examples diff --git a/.mstar/knowledge/testing-patterns/connect-identity-parity-proof.md b/.mstar/knowledge/testing-patterns/connect-identity-parity-proof.md index 888eafbd..b90455a5 100644 --- a/.mstar/knowledge/testing-patterns/connect-identity-parity-proof.md +++ b/.mstar/knowledge/testing-patterns/connect-identity-parity-proof.md @@ -1,7 +1,7 @@ --- module: spoke-connect date: 2026-08-01 -last_updated: 2026-08-04 +last_updated: 2026-09-22 problem_type: testing_pattern category: testing-patterns severity: high @@ -36,7 +36,7 @@ The parity check lives in one zero-dependency standalone script that runs locall - `tooling/connect-identity-proof/proof.mjs` — zero npm dependencies; WebCrypto `Ed25519` (`subtle.sign` / `subtle.verify`); pure-JS JCS subset and base58btc; run locally with `node tooling/connect-identity-proof/proof.mjs`; exit 0 on full pass. - Not a workspace package, not published; zero-dep and local-fixture (no npm install, no network). -- **CI-gated:** the `connect-identity` job in `.github/workflows/ci.yml` runs the proof on Node 24 (SHA-pinned `actions/setup-node`) and fails the workflow on non-zero exit. The job is path-filtered to `tooling/connect-identity-proof/**`, `packages/spoke-connect-ts/**`, and the workflow file; if the git-diff path filter itself errors, the job fails open and runs the proof anyway (the proof is cheap and zero-dep). +- **CI-gated:** the `connect-identity` job in `.github/workflows/ci.yml` runs the proof on Node 24 (SHA-pinned `actions/setup-node`) and fails the workflow on non-zero exit. It is a required PR check and therefore not path-filtered: every pull request runs the proof, because the changed-path gate it used to carry reported the context green without proving anything, and the proof is cheap and zero-dep. - Re-run it before a TypeScript connect client slice; the same vectors are the regression check for any future pure-TS or uniffi implementation. ### Gotchas (the non-obvious parts that break byte parity) diff --git a/.mstar/knowledge/tooling-decisions/ci-assembled-committed-native-artifacts.md b/.mstar/knowledge/tooling-decisions/ci-assembled-committed-native-artifacts.md index ec1bd37d..e21622d6 100644 --- a/.mstar/knowledge/tooling-decisions/ci-assembled-committed-native-artifacts.md +++ b/.mstar/knowledge/tooling-decisions/ci-assembled-committed-native-artifacts.md @@ -1,7 +1,7 @@ --- module: connect / CI native artifacts date: 2026-08-18 -last_updated: 2026-09-18 +last_updated: 2026-09-22 problem_type: tooling_decision category: tooling-decisions severity: medium @@ -27,9 +27,9 @@ This repo commits compiled native artifacts (the three-slice `spoke_connectFFI.x The pattern has four parts: -1. **Path-filtered CI assembly.** A dedicated workflow (`xcframework.yml`) builds the artifact from the checkout on every FFI-surface-affecting change (filter: connect sources, `Cargo.toml`/`Cargo.lock`, the build script, the generated binding tree). Pinned exact toolchain + `--locked` builds. `macos-14` for xcframework (needs `xcodebuild`); `actions/checkout` with `lfs: true`. +1. **Required-check CI assembly.** A dedicated workflow (`xcframework.yml`) builds the artifact from the checkout on **every pull request** — `xcframework` is a required check, and a required context a path filter skips can never be satisfied. On pushes it stays path-filtered to the FFI-surface-affecting change set (connect sources, `Cargo.toml`/`Cargo.lock`, the build script, the generated binding tree). Pinned exact toolchain + `--locked` builds. `macos-14` for xcframework (needs `xcodebuild`); `actions/checkout` with `lfs: true`. 2. **Committed artifact is the drift baseline.** A sorted per-file SHA-256 manifest diff (`verify-xcframework-drift.sh`) compares the committed artifact against the CI build; mismatch = red job. The built artifact uploads `if: always()` — a red run still ships the correct replacement. -3. **One-command apply, fail-closed.** `apply-xcframework-artifact.sh ` downloads the artifact, **mandatorily** checksum-verifies against its manifest (absent manifest = hard fail), verifies provenance (`gh run view`: expected workflow + acceptable conclusion + `headSha` match or an explicit loud override), asserts git-lfs handling, then rsyncs and stages. After committing a refresh, re-verify (a refresh-only push does not re-trigger the path-filtered gate — documented in CONTRIBUTING). +3. **One-command apply, fail-closed.** `apply-xcframework-artifact.sh ` downloads the artifact, **mandatorily** checksum-verifies against its manifest (absent manifest = hard fail), verifies provenance (`gh run view`: expected workflow + acceptable conclusion + `headSha` match or an explicit loud override), asserts git-lfs handling, then rsyncs and stages. After committing a refresh, re-verify (a refresh-only push stays outside the push-side filter; a pull request touching the artifact re-runs it — documented in CONTRIBUTING). 4. **No auto-commit.** `createCommitOnBranch` cannot carry LFS objects and `GITHUB_TOKEN` pushes do not re-trigger workflows; a maintainer applies the artifact locally with one command. That is the deliberate delivery mode — the maintainer never runs the build. Determinism lessons (both were latent bugs the CI gate exposed): @@ -53,4 +53,4 @@ Any committed compiled artifact refreshed from CI — today the Swift xcframewor ## Examples - FFI-surface PR: `xcframework.yml` runs → drift detected → red job + artifact uploaded → maintainer runs `apply-xcframework-artifact.sh ` → commit → next run green (no drift). -- Non-FFI PR: path filter skips the job — zero macOS minutes. +- Non-FFI PR: the required `xcframework` check still runs (pull-request triggers are unfiltered) — the macOS minutes are the price of a required context that must never be skipped. diff --git a/.mstar/knowledge/tooling-decisions/connect-swift-xcframework-ios-matrix.md b/.mstar/knowledge/tooling-decisions/connect-swift-xcframework-ios-matrix.md index 3f7a1ac0..cee14f30 100644 --- a/.mstar/knowledge/tooling-decisions/connect-swift-xcframework-ios-matrix.md +++ b/.mstar/knowledge/tooling-decisions/connect-swift-xcframework-ios-matrix.md @@ -1,7 +1,7 @@ --- module: spoke-connect date: 2026-08-04 -last_updated: 2026-08-18 +last_updated: 2026-09-22 problem_type: tooling_decision category: tooling-decisions severity: medium @@ -41,8 +41,8 @@ The Swift binding ships the `SpokeConnect` SwiftPM product from a **committed** - Delivered shape: three `LibraryIdentifier`s covering four target triples — `macos-arm64`, `ios-arm64`, `ios-arm64_x86_64-simulator` (arm64 + x86_64 fat). Coverage equals four discrete slices. - `xcodebuild -validate-xcframework` does not exist in Xcode 26.6 (only `-create-xcframework`). Validate with `plutil -lint` on the Info.plist, per-slice `lipo -info` arch assertions, and the consumer-path link builds below. -- The xcframework stays **committed in the repo**; between FFI changes the committed framework is the consumer artifact. Assembly runs in CI (`.github/workflows/xcframework.yml`, path-filtered on the FFI surface): the job builds the slices on `macos-14` and the committed artifact is the drift baseline — a sorted per-file SHA-256 manifest diff (`tooling/connect/verify-xcframework-drift.sh`) fails the job on mismatch, and the built artifact uploads on every run. -- Refresh path: `tooling/connect/apply-xcframework-artifact.sh ` downloads the CI artifact, checksum-verifies against its manifest, verifies run provenance, and stages the LFS pointers — no local four-target Rust build. After committing a refresh, re-run the drift check locally or via the workflow (a refresh-only push does not re-trigger the path-filtered gate). +- The xcframework stays **committed in the repo**; between FFI changes the committed framework is the consumer artifact. Assembly runs in CI (`.github/workflows/xcframework.yml`) on every pull request — `xcframework` is a required check, so its pull-request trigger is unfiltered; the push side stays path-filtered on the FFI surface: the job builds the slices on `macos-14` and the committed artifact is the drift baseline — a sorted per-file SHA-256 manifest diff (`tooling/connect/verify-xcframework-drift.sh`) fails the job on mismatch, and the built artifact uploads on every run. +- Refresh path: `tooling/connect/apply-xcframework-artifact.sh ` downloads the CI artifact, checksum-verifies against its manifest, verifies run provenance, and stages the LFS pointers — no local four-target Rust build. After committing a refresh, re-run the drift check locally or via the workflow (a refresh-only push stays outside the push-side filter, while a pull request touching the artifact re-runs it). - Build determinism: the script normalizes `Info.plist` `AvailableLibraries` ordering (xcodebuild emits it nondeterministically) and normalizes the Cargo lockstep version surfaces to `0.0.0` for every Cargo invocation, restoring them unconditionally afterward. Thus identical FFI sources produce byte-identical committed artifacts at every lockstep version. Cargo otherwise folds the package version into `-C metadata`, changing crate disambiguators and mangled symbols. - Generated Swift sources (`generated/spoke_connect.swift`, `spoke_connectFFI.h`, modulemap) stay **byte-identical** across rebuilds while the FFI surface is unchanged — the script verifies with SHA-256 before/after and must not rewrite them. - Repo hygiene: `.gitignore` covers SwiftPM artifacts (`.build/`, `.swiftpm/`) at the repo root and local packages, mirroring the C#/Kotlin artifact-ignore pattern. diff --git a/.mstar/specs/connect-binding-channels.md b/.mstar/specs/connect-binding-channels.md index 1a9a2be0..28bc8072 100644 --- a/.mstar/specs/connect-binding-channels.md +++ b/.mstar/specs/connect-binding-channels.md @@ -32,7 +32,7 @@ | Generated sources | **Committed** per language (C# `generated/spoke_connect.cs` is the reference); regenerated only when the FFI surface changes. Consumers never run bindgen. The C/C++ channel commits hand-written headers instead of generated sources | | Golden parity | Every language smoke asserts the committed Rust fixtures: golden peer id `12D3KooWJ1TsijH7H5F74hfAD5XishQz3sxrmAtVY37GtNd9CqYf`, the golden base64url hello signature, and protocol version `1` | | Lockstep SemVer | Binding manifests carry the repo `X.Y.Z`; new version surfaces register in `tooling/release/lockstep-surfaces.mjs` (assert + bump). Tag-resolved channels (SPM, Go modules, C/C++) take the version from the git tag `vX.Y.Z` itself; the C/C++ channel adds no version-bearing manifest — the header's ABI revision is independent of the repo version | -| Native provenance | The `build-connect-ffi` matrix on `release.yml` (`linux-x64`, `win-x64`, `osx-arm64`) is the single native build; registry packagers assemble per-language layouts from those artifacts. The Swift xcframework is CI-assembled on `macos-14` by the `xcframework` job ([`.github/workflows/xcframework.yml`](../../.github/workflows/xcframework.yml)) when the FFI surface changes; the Go `native/` dylibs are maintainer-built and refreshed when the FFI surface changes, not on every release. The Swift xcframework static libraries are tracked via **git-lfs** (`.gitattributes`) so refreshes do not accumulate staticlib churn in git history; SPM consumers need git-lfs (bundled with Xcode; CLI `swift build` users run `git lfs install` once). The Go `native/` dylibs stay as plain git blobs — Go modules resolve through the module proxy, which does not smudge LFS. The C/C++ carrier natives are separate from that matrix: `crates/spoke-connect/bindings/cpp/native/provenance.json` records, per RID, the source revision, target, `rustc -Vv`, compiler version, build flags, header SHA-256 and artifact SHA-256 of the committed files; `tooling/connect/cpp-build.mjs` stages them from the release source tree, and the [`.github/workflows/cpp-connect.yml`](../../.github/workflows/cpp-connect.yml) lane produces the Windows pair (§3.6) | +| Native provenance | The `build-connect-ffi` matrix on `release.yml` (`linux-x64`, `win-x64`, `osx-arm64`) is the single native build; registry packagers assemble per-language layouts from those artifacts. The Swift xcframework is CI-assembled on `macos-14` by the `xcframework` job ([`.github/workflows/xcframework.yml`](../../.github/workflows/xcframework.yml)) on every pull request and on FFI-surface pushes; the Go `native/` dylibs are maintainer-built and refreshed when the FFI surface changes, not on every release. The Swift xcframework static libraries are tracked via **git-lfs** (`.gitattributes`) so refreshes do not accumulate staticlib churn in git history; SPM consumers need git-lfs (bundled with Xcode; CLI `swift build` users run `git lfs install` once). The Go `native/` dylibs stay as plain git blobs — Go modules resolve through the module proxy, which does not smudge LFS. The C/C++ carrier natives are separate from that matrix: `crates/spoke-connect/bindings/cpp/native/provenance.json` records, per RID, the source revision, target, `rustc -Vv`, compiler version, build flags, header SHA-256 and artifact SHA-256 of the committed files; `tooling/connect/cpp-build.mjs` stages them from the release source tree, and the [`.github/workflows/cpp-connect.yml`](../../.github/workflows/cpp-connect.yml) lane produces the Windows pair (§3.6). The two carrier dynamic libraries are tracked via **git-lfs** for the same churn reason, while the import library (`*.dll.lib`) and `provenance.json` stay plain git objects that any checkout can read and diff — C/C++ consumers add `git lfs install` once (`git lfs pull` in an existing clone) | | crates.io package | `crates/spoke-connect/Cargo.toml` `exclude`s the entire `bindings/**` tree (Path B sources + natives). crates.io ships the Rust crate only; language bindings stay on their own Path B channels and the git repo | | Tag gate | Stable `vX.Y.Z` and non-`-rc.` prerelease tags publish; `-rc.` tags create GitHub pre-releases only | @@ -111,9 +111,9 @@ | Generator route | **Hand-written headers, no generator** — the maintainer writes both headers together with the Rust exports. The drift gate is executable: `node tooling/connect/cpp-symbol-check.mjs --header
--library ` parses the marked declaration block, diffs it against the native's exported `spoke_connect_*` symbols in both directions, compiles a C99 probe holding a typed function pointer to every declaration, and compiles a C++17 inclusion check over `.h` + `.hpp` (with the convenience layer's values, handles and callback factories instantiated) with exceptions and RTTI disabled | | Parity | `crates/spoke-connect/bindings/cpp/parity.md` maps every production facade member, callback and error variant to its C declaration and its C++ counterpart — 82 facade members + 15 error variants across 83 declarations, unmatched count zero in both columns | | Native provenance | `crates/spoke-connect/bindings/cpp/native/provenance.json` — per RID: source revision, target, `rustc -Vv`, compiler version, build flags, header SHA-256 and artifact SHA-256. Separate from the UniFFI `build-connect-ffi` matrix; `tooling/connect/cpp-build.mjs` stages a native from the release source tree and records the fields | -| CI lane | [`.github/workflows/cpp-connect.yml`](../../.github/workflows/cpp-connect.yml) — job `windows-smoke` on `windows-latest`, path-filtered to the carrier, the linked Rust sources, the header/smoke/scripts and the workflow itself, running on `pull_request`, `push` to `main` and `workflow_dispatch`; it builds the carrier for `x86_64-pc-windows-msvc` (stable Rust 1.96.0, MSVC, `-C target-feature=-crt-static`), runs the symbol gate and the C/C++ probes, then compiles, links and runs the shared smoke. The lane is green on Windows, and its uploaded artifacts are the committed `win-x64` pair | +| CI lane | [`.github/workflows/cpp-connect.yml`](../../.github/workflows/cpp-connect.yml) — job `windows-smoke` on `windows-latest` (plus `macos-smoke` on `macos-14` for the committed `osx-arm64` carrier), running on every `pull_request` and — path-filtered to the carrier, the linked Rust sources, the header/smoke/scripts and the workflow itself — on `push` to `main` and `workflow_dispatch`; both lanes check out with `lfs: true`, verify the committed provenance (`cpp-build.mjs --verify`) before building, run the symbol gate and the C/C++ probes, then compile, link and run the shared smoke. The Windows lane is green on Windows, and its uploaded artifacts are the committed `win-x64` pair | | Smoke | `node tooling/connect/cpp-smoke.mjs --rid osx-arm64` (Apple clang) and `--rid win-x64` (MSVC) compile, link and run `Smoke/main.cpp` (raw C ABI) and `Smoke/convenience.cpp` (convenience layer) against `crates/spoke-connect/tests/fixtures/golden-hello.json`. Each RID is built twice — exceptions disabled (the consumer default) and exceptions enabled — and each configuration must print its own ordered banner list exactly: golden peer-id, golden hello signature, protocol version 1, loopback ports, rejection/ownership, `C++ convenience values/core`, `C++ convenience callbacks/session`, `C++ convenience router`, then `C++ smoke`; the enabled configuration additionally prints `C++ callback exception containment` | -| Distribution | **Git-based** — the committed headers and platform natives resolve together from the repository tag `vX.Y.Z`, the resolution model the Go and SPM channels use; the channel has no registry feed and no Release archive assets | +| Distribution | **Git-based** — the committed headers and platform natives resolve together from the repository tag `vX.Y.Z`, the resolution model the Go and SPM channels use; the two carrier dynamic libraries are **git-lfs** objects, so a consumer clone runs `git lfs install` once (`git lfs pull` in an existing clone) while the import library and `provenance.json` arrive as plain git objects; the channel has no registry feed and no Release archive assets | | Decision record | [`connect-cpp-binding.md`](connect-cpp-binding.md) | | Non-goals | §6 records the channel exclusions; the decision record lists the same boundary (C++20 generator integration, vcpkg/Conan, Release archives, additional platform artifacts, engine-internal verification, new wire/session semantics, exposed envelope-auth helpers, async node lifecycle, cross-image handle interoperability) | diff --git a/.mstar/specs/connect-publish-strategy.md b/.mstar/specs/connect-publish-strategy.md index c83454b1..983bd40e 100644 --- a/.mstar/specs/connect-publish-strategy.md +++ b/.mstar/specs/connect-publish-strategy.md @@ -187,7 +187,7 @@ Bindings under `crates/spoke-connect/bindings/*` are packaged per language: unif | Item | Value | |------|--------| | **Conclusion** | **Remain fallback route only** — does not overturn pure-TS-minimal primary ([`spoke-connect-ts-route.md`](spoke-connect-ts-route.md)) | -| **Evidence** | **When mesh interop matters:** only for **direct libp2p-network participation** — a product host that must dial/listen on the same Noise/yamux mesh as the Rust reference spike (`crates/spoke-connect`). Envelope-level interop — the v1 goal — requires only the same hello / `peer_id` / session rules over **any ordered stream** (WebSocket today); Noise multistream is not required for it ([`spoke-connect-ts-route.md`](spoke-connect-ts-route.md) evaluation criterion 10). **Dependency weight:** js-libp2p is a deep `@libp2p/*` monorepo tree — large supply-chain and bundle surface (criterion 7), inverse of the thin `@42ch/*` helper pattern; Noise/multistream interop with rust-libp2p 0.57 additionally needs version pinning and periodic re-verify (criterion 10). **Maintenance:** … +| **Evidence** | **When mesh interop matters:** only for **direct libp2p-network participation** — a product host that must dial/listen on the same Noise/yamux mesh as the Rust reference spike (`crates/spoke-connect`). Envelope-level interop — the v1 goal — requires only the same hello / `peer_id` / session rules over **any ordered stream** (WebSocket today); Noise multistream is not required for it ([`spoke-connect-ts-route.md`](spoke-connect-ts-route.md) evaluation criterion 10). **Dependency weight:** js-libp2p is a deep `@libp2p/*` monorepo tree — large supply-chain and bundle surface (criterion 7), inverse of the thin `@42ch/*` helper pattern; Noise/multistream interop with rust-libp2p 0.57 additionally needs version pinning and periodic re-verify (criterion 8). Current package reality: `@42ch/spoke-connect` dependencies are `@42ch/spoke-operations`, `@42ch/spoke-schemas`, `@noble/ciphers`, `@noble/curves`, `@noble/ed25519`, `@noble/hashes`, `canonicalize`, `ws` — no libp2p deps. **Session-core ownership:** js-libp2p does **not** replace the TS session-core port — sequence, nonce, allowlist, `request_id` correlation, and the dispatch gate remain SDK-owned (criterion 6); the SPOKE hello is JCS-signed, not a libp2p native hello (criterion 4) | | **Recommend** | Do **not** add `js-libp2p` to `@42ch/spoke-connect` default dependencies; keep the pure-TS-minimal primary intact | | **Defer** | Shipping a mesh helper inside the default package | | **Trigger to ship a mesh helper** | Product requires shared libp2p network with the Rust spike; then prefer an **optional companion package or consumer-repo adapter**, not a forced default export | diff --git a/.mstar/specs/spoke-connect-ts-route.md b/.mstar/specs/spoke-connect-ts-route.md index bd9cbc36..f68b68ca 100644 --- a/.mstar/specs/spoke-connect-ts-route.md +++ b/.mstar/specs/spoke-connect-ts-route.md @@ -78,7 +78,7 @@ Scoring is High / Med / Low fitness for SPOKE Path A browser+Node clients that m **Location:** [`tooling/connect-identity-proof/`](../../tooling/connect-identity-proof/) (not a workspace package; not published). -**CI gate:** the proof runs via the `connect-identity` job in [`.github/workflows/ci.yml`](../../.github/workflows/ci.yml) (Node 24) on the workflow's triggers (pull requests, pushes to `main`), path-filtered to `tooling/connect-identity-proof/**` and `packages/spoke-connect-ts/**` (plus the workflow file); a non-zero proof exit fails the workflow. +**CI gate:** the proof runs via the `connect-identity` job in [`.github/workflows/ci.yml`](../../.github/workflows/ci.yml) (Node 24) on every pull request and on pushes to `main`; it is a required check, so it is deliberately not path-filtered (a changed-path gate would report the context green without running the proof); a non-zero proof exit fails the workflow. **Command:** diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1c9385cd..f8c31ae9 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -8,10 +8,12 @@ Agent and harness invariants live in [`AGENTS.md`](AGENTS.md). Normative release - Node.js ≥ 20 and pnpm ≥ 11.21 (pinned via the root packageManager field) - Rust toolchain (stable) for `spoke-schemas` / `spoke-operations` crates +- git-lfs — the committed Swift xcframework static libraries and C carrier dynamic libraries are LFS objects ```bash git clone https://github.com/42ch-dev/spoke.git cd spoke +git lfs install pnpm install ``` @@ -89,7 +91,9 @@ These stay private to the monorepo (not published to registries): ## Pull requests and CI -PRs must pass GitHub Actions jobs `verify-codegen`, `typescript`, `rust`, and `verify-version` ([`.github/workflows/ci.yml`](.github/workflows/ci.yml)). +Every pull request must pass the required checks bound to the `main` ruleset: the PR validation contexts produced by [`.github/workflows/ci.yml`](.github/workflows/ci.yml) (`verify-codegen`, `typescript`, `rust`, `verify-version`, `connect-identity`, `golden-vector-sync`, `csharp-connect`, `kotlin-connect-smoke`), `windows-smoke` and `macos-smoke` ([`cpp-connect.yml`](.github/workflows/cpp-connect.yml)), `xcframework` ([`xcframework.yml`](.github/workflows/xcframework.yml)), `Build docs site` ([`docs.yml`](.github/workflows/docs.yml)), the managed `Analyze (…)` / `CodeQL` analysis, and the Cursor Approval Agent and Greptile review checks. The pull-request triggers of those workflows are deliberately unfiltered — a path filter would leave a required context unscheduled, and an unscheduled required check can never be satisfied. + +Two maintenance rules follow. When a new PR validation check is added, register it in the `main` ruleset required checks in the same round; an unregistered check gates nothing. And a PR is ready only once every required check has finished successfully — a pending CodeQL or Greptile run is not a pass, and "no review comments" is not the same as a green review. ## Integrator docs site @@ -105,7 +109,7 @@ Pages summarize each topic and link the normative body in `.mstar/specs/` — th ## Refreshing the Swift xcframework -The three-slice `spoke_connectFFI.xcframework` is assembled in CI by the path-filtered `xcframework` job ([`.github/workflows/xcframework.yml`](.github/workflows/xcframework.yml)) on `macos-14` from the checkout's Rust sources — pinned toolchain 1.96.0, four Apple targets, `--locked` build via `tooling/connect/build-swift-xcframework.sh`. The job runs when the FFI surface changes; `tooling/connect/verify-xcframework-drift.sh` compares per-file SHA-256 hashes of the CI build against the committed (LFS) artifact and fails the job on drift. The built xcframework and its hash manifest upload on every run. +The three-slice `spoke_connectFFI.xcframework` is assembled in CI by the `xcframework` job ([`.github/workflows/xcframework.yml`](.github/workflows/xcframework.yml)) on `macos-14` from the checkout's Rust sources — pinned toolchain 1.96.0, four Apple targets, `--locked` build via `tooling/connect/build-swift-xcframework.sh`. Every pull request runs it; pushes run it when the FFI surface changes. `tooling/connect/verify-xcframework-drift.sh` compares per-file SHA-256 hashes of the CI build against the committed (LFS) artifact and fails the job on drift. The built xcframework and its hash manifest upload on every run. Refresh the committed artifact from a run in one command (requires the `gh` CLI and `rsync`): @@ -115,7 +119,7 @@ Refresh the committed artifact from a run in one command (requires the `gh` CLI The script first verifies the run's provenance — the `Xcframework` workflow, a `success` conclusion (or a `failure` caused only by the drift gate, which still ships a complete artifact), and a `headSha` matching the current checkout (override with `--allow-sha ` only to deliberately pin an older build). It then requires the artifact's hash manifest and checksum-verifies every file against it before rsyncing the built tree over the committed one and staging the LFS pointers. Commit with the suggested line — `build(connect): refresh xcframework from CI artifact ` — using normal maintainer credentials (token pushes don't re-trigger workflows and can't carry LFS objects). -A refresh-only push does not re-trigger the `xcframework` job (the committed xcframework path is off the workflow's own filter), so after committing a refreshed artifact, confirm the drift gate is still green before pushing — re-run the producing job (`gh run rerun `), or compare the artifact's built tree against the committed one locally: +A refresh-only push does not re-trigger the `xcframework` job (the committed xcframework path is off the workflow's push filter — a pull request touching it always re-runs), so after committing a refreshed artifact, confirm the drift gate is still green before pushing — re-run the producing job (`gh run rerun `), or compare the artifact's built tree against the committed one locally: ```bash gh run download --name spoke-connect-xcframework --dir /tmp/xcf-stage @@ -124,6 +128,14 @@ gh run download --name spoke-connect-xcframework --dir /tmp/xcf-stage /tmp/xcf-stage/spoke_connectFFI.xcframework ``` +## Refreshing the C carrier natives + +`crates/spoke-connect/bindings/cpp/native/` holds the two committed C carriers (`osx-arm64/libspoke_connect_capi.dylib`, `win-x64/spoke_connect_capi.dll`) with the Windows import library and `provenance.json`. The dynamic libraries are git-lfs objects; `spoke_connect_capi.dll.lib` and `provenance.json` stay ordinary Git objects, so a checkout can read and diff the record without an LFS fetch. A converted or refreshed carrier needs `git add --renormalize ` — a plain `git add` sees unchanged bytes and stages nothing. + +The Windows lane of [`cpp-connect.yml`](.github/workflows/cpp-connect.yml) checks out with `lfs: true`, builds the carrier (`node tooling/connect/cpp-build.mjs --target x86_64-pc-windows-msvc --toolchain 1.96.0`) and uploads `spoke-connect-win-x64`: the DLL, the import library and the refreshed `provenance.json`. Refresh the committed carrier by copying that artifact over `native/win-x64/` and committing the binaries together with the record that describes them — the recorded `headerSha256` / artifact hashes must match the committed bytes, never the other way round. + +`node tooling/connect/cpp-build.mjs --verify --target ` re-reads the committed `provenance.json` and re-hashes the committed header and native without building; it fails on a mismatch, and both carrier lanes run it before they build anything. The C contract header is pinned to LF (`text eol=lf` in `.gitattributes`) so its recorded `headerSha256` is identical on every platform. + ## Release Primary path — **New release** on GitHub Actions, then merge the PR: diff --git a/README.md b/README.md index b1c68d07..5a0fc774 100644 --- a/README.md +++ b/README.md @@ -132,7 +132,7 @@ pnpm add @42ch/spoke-connect cargo add spoke-connect ``` -**C and C++** — git-based from the release tag: check out `vX.Y.Z` and take `crates/spoke-connect/bindings/cpp/include/spoke_connect.h` (hand-written C ABI header), `crates/spoke-connect/bindings/cpp/include/spoke_connect.hpp` (C++17 header-only convenience layer), and the committed carrier for your target — `crates/spoke-connect/bindings/cpp/native/osx-arm64/libspoke_connect_capi.dylib` for macOS arm64 or `crates/spoke-connect/bindings/cpp/native/win-x64/spoke_connect_capi.dll` for Windows x64. +**C and C++** — git-based from the release tag: check out `vX.Y.Z` and take `crates/spoke-connect/bindings/cpp/include/spoke_connect.h` (hand-written C ABI header), `crates/spoke-connect/bindings/cpp/include/spoke_connect.hpp` (C++17 header-only convenience layer), and the committed carrier for your target — `crates/spoke-connect/bindings/cpp/native/osx-arm64/libspoke_connect_capi.dylib` for macOS arm64 or `crates/spoke-connect/bindings/cpp/native/win-x64/spoke_connect_capi.dll` for Windows x64. Both carrier libraries are Git LFS objects, so run `git lfs install` once and `git lfs pull` in an existing clone before linking. ```bash git clone --branch vX.Y.Z --depth 1 https://github.com/42ch-dev/spoke.git diff --git a/README_CN.md b/README_CN.md index b8a6062e..4b6bd989 100644 --- a/README_CN.md +++ b/README_CN.md @@ -132,7 +132,7 @@ pnpm add @42ch/spoke-connect cargo add spoke-connect ``` -**C 与 C++** —— 基于 git、与发布标签同源:检出 `vX.Y.Z`,取用 `crates/spoke-connect/bindings/cpp/include/spoke_connect.h`(手写 C ABI 头文件)、`crates/spoke-connect/bindings/cpp/include/spoke_connect.hpp`(C++17 头文件式便利层),以及目标平台的已提交载体 —— macOS arm64 用 `crates/spoke-connect/bindings/cpp/native/osx-arm64/libspoke_connect_capi.dylib`,Windows x64 用 `crates/spoke-connect/bindings/cpp/native/win-x64/spoke_connect_capi.dll`。 +**C 与 C++** —— 基于 git、与发布标签同源:检出 `vX.Y.Z`,取用 `crates/spoke-connect/bindings/cpp/include/spoke_connect.h`(手写 C ABI 头文件)、`crates/spoke-connect/bindings/cpp/include/spoke_connect.hpp`(C++17 头文件式便利层),以及目标平台的已提交载体 —— macOS arm64 用 `crates/spoke-connect/bindings/cpp/native/osx-arm64/libspoke_connect_capi.dylib`,Windows x64 用 `crates/spoke-connect/bindings/cpp/native/win-x64/spoke_connect_capi.dll`。两个载体动态库为 Git LFS 对象,链接前请先执行一次 `git lfs install`,已有 clone 再执行 `git lfs pull`。 ```bash git clone --branch vX.Y.Z --depth 1 https://github.com/42ch-dev/spoke.git diff --git a/crates/spoke-connect/bindings/cpp/README.md b/crates/spoke-connect/bindings/cpp/README.md index 1ba1401e..332b73be 100644 --- a/crates/spoke-connect/bindings/cpp/README.md +++ b/crates/spoke-connect/bindings/cpp/README.md @@ -127,11 +127,27 @@ extra, repeated or reordered banner fails the run. | `osx-arm64` | **Committed** — built and staged by `tooling/connect/cpp-build.mjs` | | `win-x64` | **Committed** — Windows x64 carrier and Rust-produced import library | +**git-lfs:** the carrier dynamic libraries (`native/osx-arm64/*.dylib`, +`native/win-x64/*.dll`) are tracked via [git-lfs](https://git-lfs.com) +(`.gitattributes`). Run `git lfs install` once per machine and `git lfs pull` in +a clone that already exists — a fresh clone smudges them automatically. The +headers, the import library (`spoke_connect_capi.dll.lib`) and `provenance.json` +are ordinary Git objects and arrive with any clone. + `tooling/connect/cpp-build.mjs` rebuilds a native from the integrated Rust source and refreshes that RID's `provenance.json` entry with the revision, toolchain and hashes of the shipped files. It accepts the two committed targets, `aarch64-apple-darwin` and `x86_64-pc-windows-msvc`. +`node tooling/connect/cpp-build.mjs --verify --target ` is the read-only +counterpart consumers run: it re-reads the committed entry and hashes the +committed header and staged native, so a copy that no longer matches the record +fails instead of being quietly re-recorded. The C contract header is pinned to +LF (`crates/spoke-connect/bindings/cpp/include/spoke_connect.h text eol=lf` in +`.gitattributes`) for that check's sake — the recorded `headerSha256` has to +describe the header's bytes on every platform, not the bytes one checkout's +end-of-line conversion happened to produce. + ## Header durability `tooling/connect/cpp-symbol-check.mjs` compares the header's declaration block diff --git a/crates/spoke-connect/bindings/cpp/native/osx-arm64/libspoke_connect_capi.dylib b/crates/spoke-connect/bindings/cpp/native/osx-arm64/libspoke_connect_capi.dylib index a5bb7b43..664240d7 100755 Binary files a/crates/spoke-connect/bindings/cpp/native/osx-arm64/libspoke_connect_capi.dylib and b/crates/spoke-connect/bindings/cpp/native/osx-arm64/libspoke_connect_capi.dylib differ diff --git a/crates/spoke-connect/bindings/cpp/native/provenance.json b/crates/spoke-connect/bindings/cpp/native/provenance.json index 62ab18ff..9fa21ff9 100644 --- a/crates/spoke-connect/bindings/cpp/native/provenance.json +++ b/crates/spoke-connect/bindings/cpp/native/provenance.json @@ -30,7 +30,7 @@ }, "win-x64": { "target": "x86_64-pc-windows-msvc", - "sourceRevision": "d93500f0dca5041630f2f6e2702844c698432636", + "sourceRevision": "f7e8c95cfcb5bf373e522964064711b8f89862a0", "rustcVersion": "rustc 1.96.0 (ac68faa20 2026-05-25)\nbinary: rustc\ncommit-hash: ac68faa20c58cbccd01ee7208bf3b6e93a7d7f96\ncommit-date: 2026-05-25\nhost: x86_64-pc-windows-msvc\nrelease: 1.96.0\nLLVM version: 22.1.2", "compilerVersion": "Microsoft (R) C/C++ Optimizing Compiler Version 19.51.36256 for x64", "buildFlags": { @@ -50,11 +50,11 @@ "target-feature=-crt-static" ] }, - "headerSha256": "a2cc902555059ce367c2f283278acad6df880eac9db2af5fbca281a5bf592a26", + "headerSha256": "7b4b4472f8a3d877978f1d0805c4acdfc6f02c99d44a1643a33a54a3aeec88a1", "artifacts": [ { "path": "spoke_connect_capi.dll", - "sha256": "2f7993d0f87832d19b4e0ca0cc8cf1e7c818413e717b29d746562add4ea00c14" + "sha256": "f292c98d0ea0969ccfb0e7a6920aba8a58f61ca987ee44202ddb0c8e0e5e6ded" }, { "path": "spoke_connect_capi.dll.lib", diff --git a/crates/spoke-connect/bindings/cpp/native/win-x64/spoke_connect_capi.dll b/crates/spoke-connect/bindings/cpp/native/win-x64/spoke_connect_capi.dll index d9f42fd5..8b01c9bd 100644 Binary files a/crates/spoke-connect/bindings/cpp/native/win-x64/spoke_connect_capi.dll and b/crates/spoke-connect/bindings/cpp/native/win-x64/spoke_connect_capi.dll differ diff --git a/docs/how-to/connect-cpp-binding.md b/docs/how-to/connect-cpp-binding.md index a2251bae..2a37aa0b 100644 --- a/docs/how-to/connect-cpp-binding.md +++ b/docs/how-to/connect-cpp-binding.md @@ -24,6 +24,15 @@ The carrier wraps the same public Rust facade the generated **native bindings** | `crates/spoke-connect/bindings/cpp/native/provenance.json` | Per RID: source revision, target, `rustc -Vv`, compiler version, build flags, header and artifact hashes | | `crates/spoke-connect/bindings/cpp/README.md` | The binding README with the full calling contract | +The two carrier libraries (`*.dylib`, `*.dll`) are stored in Git LFS — a plain clone leaves pointer files behind, so fetch the real bytes before compiling: + +```bash +git lfs install # once per machine +git lfs pull # in an existing clone; a fresh clone smudges them automatically +``` + +The headers, the import library (`spoke_connect_capi.dll.lib`) and `provenance.json` are ordinary Git objects and arrive with any clone. + The C header compiles as C99 (fixed-width integers from ``, lengths from ``) and uses `extern "C"` inclusion for C++. Windows functions and callback pointers use `__cdecl`; macOS uses the platform default C calling convention. Every public symbol is prefixed `spoke_connect_`. The convenience header needs only the C++17 standard library and defines no configuration macro of its own. ## 2. Compile and link diff --git a/docs/how-to/connect-native-bindings.md b/docs/how-to/connect-native-bindings.md index 2dd607d7..45f6c4e4 100644 --- a/docs/how-to/connect-native-bindings.md +++ b/docs/how-to/connect-native-bindings.md @@ -135,6 +135,8 @@ Binding README: [`bindings/python/README.md`](https://github.com/42ch-dev/spoke/ ```bash git clone --branch vX.Y.Z --depth 1 https://github.com/42ch-dev/spoke.git +git lfs install # once per machine +git lfs pull # fetch the carrier libraries; a fresh clone smudges them automatically ``` - `crates/spoke-connect/bindings/cpp/include/spoke_connect.h` — C99 ABI header @@ -142,7 +144,7 @@ git clone --branch vX.Y.Z --depth 1 https://github.com/42ch-dev/spoke.git - `crates/spoke-connect/bindings/cpp/native/osx-arm64/libspoke_connect_capi.dylib` — macOS arm64 carrier - `crates/spoke-connect/bindings/cpp/native/win-x64/spoke_connect_capi.dll` — Windows x64 carrier -The committed C/C++ carriers target macOS arm64 (`osx-arm64`) and Windows x64 (`win-x64`). Take `spoke_connect.h`, `spoke_connect.hpp`, and the native files for your target from the same repository tag `vX.Y.Z`. +The committed C/C++ carriers target macOS arm64 (`osx-arm64`) and Windows x64 (`win-x64`). Take `spoke_connect.h`, `spoke_connect.hpp`, and the native files for your target from the same repository tag `vX.Y.Z`. The two carrier libraries are Git LFS objects (`.gitattributes`); the headers, the Windows import library and `provenance.json` are ordinary Git objects. The C++17 header is header-only and includes the C header. `spoke::connect` wraps the same session core in move-only RAII handles, an explicit `Result` error channel, borrowed text views over returned buffers, and host callback bridges for the transport, ports and tool surfaces. The C99 header stays the ABI contract for C hosts — status values plus the raw record and callback-table layout. diff --git a/docs/packages/quick-start.md b/docs/packages/quick-start.md index e97d693c..5687f6f7 100644 --- a/docs/packages/quick-start.md +++ b/docs/packages/quick-start.md @@ -53,6 +53,8 @@ Native bindings for host languages (C# NuGet `42ch.Spoke.Connect`, Kotlin Maven ```bash git clone --branch vX.Y.Z --depth 1 https://github.com/42ch-dev/spoke.git +git lfs install # once per machine +git lfs pull # fetch the carrier libraries; a fresh clone smudges them automatically ``` - **`crates/spoke-connect/bindings/cpp/include/spoke_connect.h`** — the hand-written C ABI header. diff --git a/docs/zh/how-to/connect-cpp-binding.md b/docs/zh/how-to/connect-cpp-binding.md index 57f01627..fef34556 100644 --- a/docs/zh/how-to/connect-cpp-binding.md +++ b/docs/zh/how-to/connect-cpp-binding.md @@ -24,6 +24,15 @@ C/C++ 渠道提供两个手写头文件与每个平台一个已提交载体。`s | `crates/spoke-connect/bindings/cpp/native/provenance.json` | 每个 RID:源码修订、target、`rustc -Vv`、编译器版本、构建标志、头文件与产物哈希 | | `crates/spoke-connect/bindings/cpp/README.md` | 绑定 README(完整调用契约) | +两个载体动态库(`*.dylib`、`*.dll`)存放在 Git LFS 中 —— 普通 clone 只会留下指针文件,编译前请先取回真实二进制: + +```bash +git lfs install # 每台机器执行一次 +git lfs pull # 已存在的 clone 执行;全新 clone 会自动 smudge +``` + +头文件、导入库(`spoke_connect_capi.dll.lib`)与 `provenance.json` 是普通 Git 对象,任何 clone 都会带上。 + C 头文件以 C99 编译(`` 的定宽整数、`` 的长度类型),C++ 通过 `extern "C"` 包含。Windows 上的函数与回调指针使用 `__cdecl`,macOS 使用平台默认 C 调用约定。所有公开符号都以 `spoke_connect_` 为前缀。便利头文件只依赖 C++17 标准库,且不定义自己的任何配置宏。 ## 2. 编译与链接 diff --git a/docs/zh/how-to/connect-native-bindings.md b/docs/zh/how-to/connect-native-bindings.md index 95306d67..075715d4 100644 --- a/docs/zh/how-to/connect-native-bindings.md +++ b/docs/zh/how-to/connect-native-bindings.md @@ -135,6 +135,8 @@ version = spoke_connect.protocol_version() # 1 ```bash git clone --branch vX.Y.Z --depth 1 https://github.com/42ch-dev/spoke.git +git lfs install # 每台机器执行一次 +git lfs pull # 取回载体动态库;全新 clone 会自动 smudge ``` - `crates/spoke-connect/bindings/cpp/include/spoke_connect.h` —— C99 ABI 头文件 @@ -142,7 +144,7 @@ git clone --branch vX.Y.Z --depth 1 https://github.com/42ch-dev/spoke.git - `crates/spoke-connect/bindings/cpp/native/osx-arm64/libspoke_connect_capi.dylib` —— macOS arm64 载体 - `crates/spoke-connect/bindings/cpp/native/win-x64/spoke_connect_capi.dll` —— Windows x64 载体 -提交的 C/C++ 载体面向 macOS arm64(`osx-arm64`)与 Windows x64(`win-x64`)。`spoke_connect.h`、`spoke_connect.hpp` 与目标平台的原生文件都取自同一仓库 tag `vX.Y.Z`。 +提交的 C/C++ 载体面向 macOS arm64(`osx-arm64`)与 Windows x64(`win-x64`)。`spoke_connect.h`、`spoke_connect.hpp` 与目标平台的原生文件都取自同一仓库 tag `vX.Y.Z`。两个载体动态库是 Git LFS 对象(`.gitattributes`);头文件、Windows 导入库与 `provenance.json` 是普通 Git 对象。 C++17 头文件为 header-only,并包含 C 头文件。`spoke::connect` 把同一会话核心包装为可移动的 RAII 句柄、显式的 `Result` 错误通道、返回缓冲区的借用文本视图,以及传输、ports 与工具面的宿主回调桥。C99 头文件仍是 C 宿主的 ABI 契约 —— 状态值加原始记录与回调表布局。 diff --git a/docs/zh/packages/quick-start.md b/docs/zh/packages/quick-start.md index 9331b3b7..87cb16de 100644 --- a/docs/zh/packages/quick-start.md +++ b/docs/zh/packages/quick-start.md @@ -63,7 +63,7 @@ git clone --branch vX.Y.Z --depth 1 https://github.com/42ch-dev/spoke.git - **`crates/spoke-connect/bindings/cpp/native/win-x64/spoke_connect_capi.dll`** —— Windows x64 载体。 - **`crates/spoke-connect/bindings/cpp/native/win-x64/spoke_connect_capi.dll.lib`** —— Windows x64 链接步骤所用的 Rust 生成导入库。 -已提交的 C/C++ 载体面向 macOS arm64(`osx-arm64`)与 Windows x64(`win-x64`)。`spoke_connect.h`、`spoke_connect.hpp` 与目标平台的原生文件均取自同一仓库标签 `vX.Y.Z` —— 获取方式是一次标签检出,`cargo add spoke-connect` 安装的是 crates.io 上的 crate。完整指南:[从 C 与 C++ 连接](/zh/how-to/connect-cpp-binding)。 +已提交的 C/C++ 载体面向 macOS arm64(`osx-arm64`)与 Windows x64(`win-x64`)。`spoke_connect.h`、`spoke_connect.hpp` 与目标平台的原生文件均取自同一仓库标签 `vX.Y.Z` —— 获取方式是一次标签检出,`cargo add spoke-connect` 安装的是 crates.io 上的 crate。两个载体动态库是 Git LFS 对象,请先执行一次 `git lfs install`,已有 clone 再执行 `git lfs pull`。完整指南:[从 C 与 C++ 连接](/zh/how-to/connect-cpp-binding)。 ## 集成路径 diff --git a/tooling/connect-identity-proof/README.md b/tooling/connect-identity-proof/README.md index d54d800f..cd3f736e 100644 --- a/tooling/connect-identity-proof/README.md +++ b/tooling/connect-identity-proof/README.md @@ -28,4 +28,4 @@ Exit `0` on full pass; non-zero on any mismatch. ## CI gate -The `connect-identity` job in [`.github/workflows/ci.yml`](../../.github/workflows/ci.yml) runs the proof on Node 24 for pull requests and pushes to `main`, path-filtered to `tooling/connect-identity-proof/**` and `packages/spoke-connect-ts/**` (plus the workflow file). A non-zero proof exit fails the job and the workflow. +The `connect-identity` job in [`.github/workflows/ci.yml`](../../.github/workflows/ci.yml) runs the proof on Node 24 for every pull request and for pushes to `main`. It is a required PR check and therefore deliberately **not** path-filtered: a changed-path gate would report the context green without running the proof, which is indistinguishable from a real pass. A non-zero proof exit fails the job and the workflow. diff --git a/tooling/connect/cpp-build.mjs b/tooling/connect/cpp-build.mjs index 345df2fc..5323dcba 100755 --- a/tooling/connect/cpp-build.mjs +++ b/tooling/connect/cpp-build.mjs @@ -33,9 +33,18 @@ * Usage: * node tooling/connect/cpp-build.mjs --target aarch64-apple-darwin --toolchain nightly * node tooling/connect/cpp-build.mjs --target x86_64-pc-windows-msvc --toolchain 1.96.0 + * node tooling/connect/cpp-build.mjs --verify --target aarch64-apple-darwin * * `--toolchain ` is passed to cargo as `+`; when omitted, the * caller's default toolchain is used. + * + * `--verify` is the read-only consumer check: it re-reads the committed + * provenance and hashes the committed header and staged native for one RID, + * failing when a recorded SHA-256 no longer describes those bytes. It builds + * nothing and writes nothing, so it runs against any checkout — including one + * whose header bytes drifted from the record (CRLF from a Windows + * `core.autocrlf=true` checkout), which is the drift the build path hides by + * rewriting the record it just hashed. */ import { createHash } from "node:crypto"; @@ -93,7 +102,7 @@ function fail(message) { } function parseArgs(argv) { - const args = { target: null, toolchain: null }; + const args = { target: null, toolchain: null, verify: false }; for (let index = 0; index < argv.length; index += 1) { const flag = argv[index]; if (flag === "--target") { @@ -102,14 +111,19 @@ function parseArgs(argv) { } else if (flag === "--toolchain") { args.toolchain = argv[index + 1]; index += 1; + } else if (flag === "--verify") { + args.verify = true; } else { - fail(`unknown argument '${flag}' (usage: --target [--toolchain ])`); + fail(`unknown argument '${flag}' (usage: --target [--toolchain ] [--verify])`); } } if (!args.target) fail("--target is required"); if (!TARGETS[args.target]) { fail(`unsupported target '${args.target}' (supported: ${Object.keys(TARGETS).join(", ")})`); } + if (args.verify && args.toolchain) { + fail("--verify only reads the committed files; drop --toolchain rather than ignoring it"); + } return args; } @@ -240,10 +254,60 @@ export function readProvenance(path) { return provenance; } +/** + * The committed entry for `spec.rid` has to describe the bytes a consumer + * receives: the contract header and every staged native the entry lists. The + * read-only shape is the point — the build path records what it just hashed, + * so only a reader can catch a checkout whose bytes no longer match the record. + */ +function verifyProvenance(spec) { + const entry = readProvenance(PROVENANCE).nativeArtifacts[spec.rid]; + if (!entry) { + fail(`provenance has no '${spec.rid}' entry, so nothing records what was shipped`); + } + + const problems = []; + const headerSha256 = sha256(HEADER); + if (entry.headerSha256 !== headerSha256) { + problems.push( + `header: recorded ${entry.headerSha256}, committed bytes ${headerSha256} ` + + `(${relative(REPO_ROOT, HEADER)})`, + ); + } + for (const artifact of spec.artifacts) { + const recorded = (entry.artifacts ?? []).find((item) => item.path === artifact.staged); + const actual = sha256(join(CPP_DIR, "native", spec.rid, artifact.staged)); + if (!recorded) { + problems.push(`native/${spec.rid}/${artifact.staged}: missing from the entry`); + } else if (recorded.sha256 !== actual) { + problems.push( + `native/${spec.rid}/${artifact.staged}: recorded ${recorded.sha256}, ` + + `committed bytes ${actual}`, + ); + } + } + + if (problems.length > 0) { + fail( + `committed provenance does not describe the committed ${spec.rid} bytes:\n ` + + problems.join("\n "), + ); + } + console.log( + `cpp-build: verified ${spec.rid} provenance (header ${headerSha256}, ` + + `natives ${spec.artifacts.length})`, + ); +} + function main() { const args = parseArgs(process.argv.slice(2)); const spec = TARGETS[args.target]; + if (args.verify) { + verifyProvenance(spec); + return; + } + assertBuildSourcesCommitted(); const cargoArgs = ["build", "--locked", "-p", CARRIER_PACKAGE, "--release", "--target", args.target, "--target-dir", TARGET_DIR_FLAG];