@astram/guide 0.1.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CAPABILITIES.md CHANGED
@@ -1,58 +1,189 @@
1
1
  <!--
2
- @astram/guide — CAPABILITIES.md (framework-owned, version 0.1.0)
2
+ @astram/guide — CAPABILITIES.md (framework-owned, version 0.2.0)
3
3
 
4
4
  What the Astram framework provides THIS version. Refreshed on upgrade (ADR-0025).
5
5
  Use this to answer "does the framework already do X?" before building — and, if it
6
6
  does NOT, that's a Rule-4 gap (STOP and flag in .astram/guide/gaps.md).
7
+
8
+ Matches the framework release: @astram/core 1.4 · guard 0.8 · engine 0.4 · identity 0.5 ·
9
+ frontend-core 0.5 · migrate 0.3 · sdk 0.2 · create-astram-app 0.6.
7
10
  -->
8
11
 
9
- # Astram Framework — Capabilities (v0.1.0)
12
+ # Astram Framework — Capabilities (v0.2.0)
13
+
14
+ The building blocks the framework provides. Consume these; do not reimplement them. Everything
15
+ below is wired by the two `forRoot` calls in your `app.module.ts`; you write no security code.
16
+
17
+ ## Wiring (the ONE seam, plus the identity module)
18
+
19
+ ```ts
20
+ import { ShieldModule } from '@astram/guard';
21
+ import {
22
+ IdentityFeatureModule, identityAuthActionKeys, identityControlPlaneKeys,
23
+ buildIdentityRecordAuthorization,
24
+ } from '@astram/identity';
25
+ // Raw-pool factory: composition root only (app.module.ts), never exported from it.
26
+ import { createIdentityAuthzPool } from '@astram/identity/internal';
27
+
28
+ const rootIds = ['<platform-admin user uuid>']; // from config, never from a request
29
+ const authzPool = createIdentityAuthzPool();
10
30
 
11
- The building blocks the framework provides. Consume these; do not reimplement them.
31
+ imports: [
32
+ ShieldModule.forRoot({
33
+ auth: true,
34
+ emailSender: myEmailSender, // or 'disabled' — REQUIRED outside dev/test
35
+ rootIds,
36
+ identityActionKeys: identityAuthActionKeys,
37
+ identityControlPlaneKeys,
38
+ recordAuthorization: buildIdentityRecordAuthorization(authzPool, [/* YourType */]),
39
+ }),
40
+ IdentityFeatureModule.forRoot({ rootIds, extraObjects: [/* YourType */] }),
41
+ ],
42
+ ```
43
+
44
+ - **`ShieldModule.forRoot(options?)`** (`@astram/guard`; `forRootAsync` for DI-computed options).
45
+ Options: `auth?`, `recordAuthorization?` (or a pre-built `decisionAuthority?`, never both),
46
+ `emailSender?: EmailSender | 'disabled'`, `rootIds?`, `identityActionKeys?`,
47
+ `identityControlPlaneKeys?`, `config?`, and optional overrides `credentialResolver?`,
48
+ `profileResolver?`, `credentialWriter?`, `resetTokenStore?`, `recoveryCodeStore?`,
49
+ `profileWriter?`, `sessionSweeper?`, `auditSink?`, `mfaDeviceStore?`, `totpVerifier?`,
50
+ `sessionFactsProjector?`, `scopeSubtreeResolver?`, `membershipChecker?`. With `auth: true` every
51
+ override has a MariaDB/Redis default; leave them out unless you have a reason.
52
+ - **`IdentityFeatureModule.forRoot({ rootIds?, extraObjects? })`** (`@astram/identity`). Pass the
53
+ SAME `rootIds` as `ShieldModule`, and the SAME `extraObjects` as
54
+ `buildIdentityRecordAuthorization`. Without `rootIds` it logs a boot warning.
55
+ - `ShieldModule.forRoot()` with no options = enforcement only (no DB, no Redis, boots without
56
+ `.env`). Omitting `recordAuthorization` (and `decisionAuthority`) binds a stub that denies every
57
+ `@Classified` route.
12
58
 
13
59
  ## Enforcement (`@astram/guard`)
14
60
  - **Global deny-by-default guard** — every route is denied unless `@Public()` or
15
- `@Classified({ typeKey, action })`.
16
- - **Request authentication** — a `@Classified` route resolves the bearer access token
17
- → `Principal` → `decide()` automatically. `request.principal` is available to the
18
- handler (act as the logged-in user).
19
- - **The ONE seam** — `ShieldModule.forRoot({ auth?, credentialResolver?, decisionAuthority?, config? })`.
20
-
21
- ## Auth surface (`@astram/guard`, opt-in `auth: true`)
22
- - `POST /auth/login` `{ identityHint, password }` → `{ session, tokens }` (or an MFA challenge).
23
- - `POST /auth/mfa` · `POST /auth/refresh` · `POST /auth/logout`.
24
- - Argon2id password verification, sessions, refresh-token rotation + reuse detection,
25
- opaque access tokens bound to the principal (server-revocable; logout = immediate kill).
26
- - MFA/TOTP.
27
- - You provide: a `CredentialResolver` (reads your users) + a `DecisionAuthority` (your policy).
61
+ `@Classified({ typeKey, action })`. With `IdentityFeatureModule` wired, an unclassified route
62
+ aborts boot (`verifyGatable`).
63
+ - **One decision authority** — a `@Classified` route resolves the bearer token → `Principal` →
64
+ `decide()`; `request.principal` is the caller. Identity, tenant, roles and root status come from
65
+ the session, never from the request.
66
+ - **Redis-only request path** — session, cached facts and the per-tenant permission version are
67
+ read from Redis; no DB query on a request with fresh facts. Redis unavailable ⇒ deny (fail
68
+ closed), bounded by `REDIS_COMMAND_TIMEOUT_MS` (default 2000).
69
+ - **Live roles** — a role posting change, a grant change or a user disable bumps the permission
70
+ version; the next request reloads facts and roles. No re-login needed.
71
+ - **Engine errors → HTTP** — `AuthorizationDeniedError` → 403, `ValidationError` → 400,
72
+ `EngineUnavailableError` → 503 for your controllers too (global filter; id-free messages).
73
+
74
+ ## Authentication and sessions (`@astram/guard`, `auth: true`)
75
+ - `POST /auth/login` `{ identityHint, password }` → `{ session, tokens }`, or an MFA challenge.
76
+ `POST /auth/mfa` (TOTP or recovery code) · `POST /auth/refresh` · `POST /auth/logout` ·
77
+ `GET /auth/me` · `POST /auth/scope-switch` · `POST /auth/forgot-password` ·
78
+ `POST /auth/reset-password`.
79
+ - `/account/*`: change password, edit profile, MFA devices (enrol, confirm, rename, delete),
80
+ regenerate recovery codes. A recovery-code login is restricted until a new device is enrolled.
81
+ - Argon2id passwords; opaque access tokens bound in Redis (server-revocable, logout is immediate);
82
+ refresh-token rotation with reuse detection (a replay revokes the whole lineage and its live
83
+ access tokens). An access token lives at most the access TTL (15 min default).
84
+ - **MFA enforced**: any user with a confirmed TOTP device must pass `/auth/mfa` on every password
85
+ login. 5 wrong codes / 15 min per account; a used code cannot be replayed (90 s); the pending
86
+ challenge lives in Redis (works across instances, never holds the password).
87
+ - **Lockout and rate limits**: per-account login and forgot-password budgets (keyed on the
88
+ resolved account, so spelling variants share one counter); per-IP limits for login,
89
+ forgot-password and reset-password, active only when Express `trust proxy` is set explicitly
90
+ (`TRUST_PROXY` in the scaffold). Rate limiters fail open on a Redis error by design; the request
91
+ path and the TOTP replay guard fail closed.
92
+ - **Disabled accounts** cannot log in or refresh; disabling a user revokes their sessions and
93
+ drops their roles at once.
94
+ - **Silent recovery**: after a Redis loss, the next refresh rebuilds the session from MariaDB
95
+ (no forced re-login); a restricted (recovery-code) session rebuilds restricted.
96
+ - **Email**: reset links go through your `EmailSender` (`send({ to, subject, html, text })`).
97
+ Outside `NODE_ENV=development|test`, `auth: true` without `emailSender` fails boot;
98
+ `'disabled'` sends nothing and `/auth/forgot-password` still answers 204.
99
+ - **Service-account API keys** (`Authorization: Bearer ask_…`) authenticate agents and machine
100
+ clients through the same `decide()` path; scope, status and roles are read live; issuing a key
101
+ needs an explicit expiry (`expiresAtMs`) or `neverExpires: true`.
102
+ - Override contracts: the login lockout is keyed on the account the injected `credentialResolver`
103
+ returns for the hint as typed, so use that one resolver wherever login resolves accounts; a
104
+ custom `totpVerifier` must use a window of at most ±1 step and must not normalise the code, or
105
+ the 90 s replay claim no longer covers it.
106
+
107
+ ## Identity and tenancy (`@astram/identity`)
108
+ - Admin API under `/identity/*`: companies (create, update name/status, move via
109
+ `PUT /identity/companies/:uuid/parent`, delete), users, roles, memberships and role postings,
110
+ access (role ↔ permission grants and access bundles), permission catalog, record policies
111
+ (guided rule builder, 14 operators, no raw expressions), record shares (owner shares),
112
+ cross-tenant grants (offer, accept, revoke), service accounts (roles, scope, keys), audit read.
113
+ `GET /identity/auth-facts` feeds the frontend's permission-gated navigation.
114
+ - **Scoped-hierarchy tenancy** (ADR-0027): a tenant is a company node; an admin's authority covers
115
+ its subtree. Reads, updates, deletes AND creates of subtree-scoped types are walled in the query
116
+ (ADR-0030, ADR-0034). Non-root admins cannot create top-level tenants.
117
+ - **No escalation**: you can grant, revoke or assign (via postings) only roles and keys you hold
118
+ (ADR-0029); a role used outside your subtree is root-only; changing another user's email or
119
+ status needs dominance over that user. Grant changes are audited in the same transaction.
120
+ - **Cross-tenant access** only through an explicit, consented, scoped, revocable grant (ADR-0009).
121
+ - **Root** = the `rootIds` allowlist only. Root administers the control plane and cannot read
122
+ customer business data.
123
+ - Migrations for `shield_identity`, `shield_authz`, `shield_app` identity tables ship in
124
+ `@astram/identity` and `@astram/guard` and run through `@astram/migrate` automatically. Dev seeds
125
+ (099/100) run only with `NODE_ENV=development` or `ASTRAM_SEED_DEV=1`.
28
126
 
29
127
  ## The record pipe (`@astram/engine`)
30
- - **`RecordEngine`** (token `RECORD_ENGINE`) — create/read/update/delete/list over
31
- registered tables, through one unbypassable pipe: permission gate (`decide()`) →
32
- server-trusted write-context (anti-forgery) → validate → write (real table) →
33
- junction sync → audit → event.
34
- - **Cross-tenant isolation** — a `tenantId`-trusted field auto-scopes reads AND writes
35
- to the actor's tenant.
36
- - **Physical wall** — the raw DB pool is sealed in the engine; feature scope never
37
- receives it. Off-road DB access fails the build (the app's `eslint.config.js`).
38
- - **Gap signal** — an unsupported field type / operation throws
39
- `CapabilityNotSupportedError` (a Rule-4 STOP).
40
- - Field types supported this version: `string`, `number`, `boolean`, `uuid`,
41
- `timestamp`, `json`. (Anything else → flag a gap.)
128
+ - **`RecordEngine`** (token `RECORD_ENGINE`) / `RecordsService` — create/get/update/delete/list
129
+ over registered object types through one pipe: permission gate (`decide()`) → server-trusted
130
+ write context → validate → write → junction sync → audit (mandatory) → event.
131
+ - **Record decision inputs**: `get`, `update` and `delete` read the stored row first and pass its
132
+ fields and tenant to `decide()`, so record rules such as "deny update where status eq locked"
133
+ hold on single-record paths. An unreachable id (missing, other tenant, outside your subtree) is
134
+ a 403, identical for missing and foreign.
135
+ - **List scoping in the query** (ADR-0028): `list()` returns exactly the rows `decide()` allows
136
+ (record rules, owner shares, cross-tenant grants, subtree), filtered in SQL; `get()` uses the
137
+ same scope. Membership/subtree-scoped lists ("companies I belong to") are provided.
138
+ - Stable paging: every `list()` orders by the primary key last.
139
+ - `transaction(fn)` runs several pipe calls in one unit of work (rolls back on any failed write);
140
+ `lockForUpdate(typeKey, id)` serialises a check-then-write.
141
+ - `@Field({ immutable: true })` create-only fields; `trusted` fields are stamped server-side.
142
+ `readScope` declares the subtree wall; `create: 'unscoped'` opts a type out of the create wall.
143
+ - **Physical wall** — the raw DB pool is sealed in the engine. The scaffold's
144
+ `server/eslint.config.mjs` fails lint on driver imports, raw pools, `.query()` and the
145
+ framework's internal DB exports anywhere in `src/**` except `app.module.ts` and `main.ts`. A lint
146
+ rule is a backstop, not a sandbox: do not go around it.
147
+ - **Gap signal** — an unsupported field type or operation throws `CapabilityNotSupportedError`
148
+ (a Rule-4 STOP). Field types this version: `string`, `number`, `boolean`, `uuid`, `timestamp`,
149
+ `json`.
42
150
 
43
151
  ## Migrations (`@astram/migrate`)
44
- - Deploy-time code-table migration runner: timestamped `up()`/`down()` files, a
45
- `schema_migrations` table, `migrate` / `migrate:revert` / `migrate:status`. Raw SQL
46
- through a DDL-capable deploy account (the running app never holds DDL).
152
+ - `migrate` / `migrate:revert` / `migrate:status` with the deploy-time DDL account (`DDL_DB_*`);
153
+ the running app never holds DDL. Framework migrations merge with yours (`APP_MIGRATION_DIR`).
154
+ - Numeric ordering, duplicate-id detection, a database lock so concurrent deploys never apply a
155
+ migration twice (`APP_MIGRATION_LOCK_TIMEOUT`, default 120 s), and drift detection (a changed
156
+ applied file is reported; `--strict` fails on it). Never edit a migration that already ran.
157
+
158
+ ## Frontend (`@astram/frontend-core`, `@astram/sdk`, scaffolded `frontend/`)
159
+ - `@astram/frontend-core`: auth, account, identity, audit, record-policy, share, cross-tenant and
160
+ service-account services over an injected `HttpClient`; `createRefreshingHttp` refreshes an
161
+ expired access token transparently (single-flight, cross-tab lock, one retry; an outage keeps
162
+ the session). Clients that call `/auth/refresh` concurrently without single-flight can log the
163
+ user out (reuse detection).
164
+ - `@astram/sdk`: platform-agnostic auth-fact projection (`check` → allowed/deny/unavailable,
165
+ version-staleness refetch, retry/backoff). The UI never re-decides.
166
+ - The scaffolded frontend (`create-astram-app --with-frontend` or `npx -p create-astram-app
167
+ astram-add-frontend`) ships login, MFA, recovery, forgot/reset, account and admin pages, with a
168
+ strict Content-Security-Policy.
47
169
 
48
- ## Frontend SDK (`@astram/sdk`)
49
- - Platform-agnostic fact projection for building UIs against the framework.
170
+ ## Available, but NOT wired to a data path (do not assume they protect data yet)
171
+ - **Envelope encryption / per-tenant DEKs** (`createEncryptionService`, `LocalKekCrypto`): the
172
+ engine exists, but no record field is encrypted and no production `DekStore` ships.
173
+ - **Secrets provider** (`SecretsProvider`, `resolveSecretRefs`, `EncryptedFileSecretsProvider`):
174
+ the guard's runtime config does not resolve `secret-ref:` values at boot.
175
+ - **Risk engine** (`createRiskEngine`): advisory scoring with no request-path caller.
176
+ - **Hash-chained audit** (`createAuditRecorder`, `verifyChain`): the shipped audit writer is
177
+ append-only but does not store the chain.
50
178
 
51
- ## NOT provided yet (as of v0.1.0) — these are Rule-4 gaps if you need them
179
+ ## NOT provided yet — these are Rule-4 gaps if you need them
52
180
  - Runtime (no-code) object definition / admin-defined objects + runtime DDL.
53
181
  - Formula / lookup / rollup computed fields (the pipe's compute stage is a no-op).
54
182
  - Business-rule events (the pipe's emit-event stage is a no-op).
55
183
  - Extension-field sidecar (runtime custom fields on a registered table).
56
- - Membership-scoped list filtering (e.g. "companies I belong to").
184
+ - Per-principal column masking on reads (row-level scoping is provided; column-level is not).
185
+ - External identity providers (OIDC, SAML, LDAP, Entra ID, Cognito, Okta, Google Workspace) and
186
+ WebAuthn ceremonies.
187
+ - Tenant- or role-mandated MFA (MFA is enforced for users who enrolled a device).
57
188
 
58
189
  If you need one of these, STOP and flag it in `.astram/guide/gaps.md` (Rule 4).
@@ -1,5 +1,5 @@
1
1
  <!--
2
- @astram/guide — FRAMEWORK_RULES.md (framework-owned, version 0.1.0)
2
+ @astram/guide — FRAMEWORK_RULES.md (framework-owned, version 0.2.0)
3
3
 
4
4
  This file is the FRAMEWORK layer of the two-layer AI guide (ADR-0025). It is
5
5
  installed in node_modules and REPLACED WHOLESALE on `npm update` — never edit it
@@ -30,10 +30,12 @@ the current rules, not a stale memory of them. Your project's own rules in
30
30
  ## 1. NEVER edit or bypass the framework
31
31
 
32
32
  The framework packages (`@astram/core`, `@astram/guard`, `@astram/engine`,
33
- `@astram/migrate`, `@astram/sdk`, `@astram/guide`) live in `node_modules` and are
34
- **immutable**. Do not edit them, patch them, monkey-patch them, or reach around their
35
- public API. You register your app through the ONE seam — `ShieldModule.forRoot(...)` —
36
- and consume the exported building blocks. That is the entire contract.
33
+ `@astram/identity`, `@astram/migrate`, `@astram/frontend-core`, `@astram/sdk`,
34
+ `@astram/guide`) live in `node_modules` and are **immutable**. Do not edit them, patch
35
+ them, monkey-patch them, or reach around their public API. You register your app through
36
+ the ONE seam — `ShieldModule.forRoot(...)`, plus `IdentityFeatureModule.forRoot(...)` for
37
+ the identity surface — and consume the exported building blocks. That is the entire
38
+ contract.
37
39
 
38
40
  - **`@astram/core` is especially sacred.** It is the pure security engine. It has no
39
41
  reason to be imported directly by feature code and must never be modified.
@@ -48,8 +50,12 @@ the permission gate, validation, audit, and cross-tenant isolation on every writ
48
50
  by construction.
49
51
 
50
52
  This is enforced two ways: the raw DB pool is **sealed inside the engine module** (you
51
- cannot inject what you are not given), and the app's `eslint.config.js` **fails the
52
- build** on any off-road DB access under `src/features/**`. If you find yourself
53
+ cannot inject what you are not given) and no package entry point exports a raw pool or
54
+ unit of work (they live on `@astram/*/internal` subpaths for `app.module.ts` wiring only);
55
+ and the app's `server/eslint.config.mjs` **fails lint** on off-road DB access anywhere in
56
+ `src/**` (only `app.module.ts` and `main.ts` may import `@astram/*/internal` and wire
57
+ infrastructure, and they may not re-export it). The lint wall is a backstop, not a
58
+ sandbox: code that slips past it is still a Rule 2 violation. If you find yourself
53
59
  wanting a raw query, see Rule 4.
54
60
 
55
61
  ## 3. Classify every route (deny-by-default)
@@ -60,9 +66,10 @@ Every HTTP route is DENIED by the global enforcement guard unless you classify i
60
66
  request's bearer token → `Principal` → `decide()` and allows only on `allow`. No
61
67
  token → 401; denied → 403.
62
68
 
63
- An unclassified route is a 403, by design — a forgotten declaration locks the door
64
- instead of silently exposing it. Never make a route public to "make it work"; classify
65
- it correctly.
69
+ An unclassified route is a 403, by design, and once `IdentityFeatureModule` is wired the
70
+ app refuses to boot with one — a forgotten declaration locks the door instead of
71
+ silently exposing it. Never make a route public to "make it work"; classify it
72
+ correctly.
66
73
 
67
74
  ## 4. When the framework can't do it: STOP and FLAG the gap — do NOT work around it
68
75
 
@@ -87,17 +94,21 @@ signal, not something to catch-and-work-around.
87
94
  Resolve internal ids to human-readable labels before they reach a UI or a response a
88
95
  user sees. Never put a password, hash, token, or secret in a response body or a log.
89
96
 
90
- ## 6. Auth is the framework's; you provide only DATA + POLICY
91
-
92
- You do not write login/session/token code — the framework owns it (`ShieldModule
93
- .forRoot({ auth: true })` gives you `POST /auth/{login,mfa,refresh,logout}` for free).
94
- Your app contributes exactly two things through the seam:
95
- - a **`CredentialResolver`** — reads YOUR user/identity tables to answer "what's this
96
- user's password hash, tenant, roles?",
97
- - a **`DecisionAuthority`** — your allow/deny policy.
98
-
99
- Never reimplement session handling, token verification, or password hashing in app
100
- code. If the auth surface lacks something you need, that's Rule 4 (flag the gap).
97
+ ## 6. Auth and identity are the framework's; you provide only CONFIG + DATA
98
+
99
+ You do not write login/session/token/MFA code, user or role administration, or an
100
+ authorization policy engine — the framework owns all of it. `ShieldModule.forRoot({ auth:
101
+ true, recordAuthorization, … })` gives you `/auth/*` and `/account/*`, and
102
+ `IdentityFeatureModule.forRoot(...)` gives you `/identity/*` (companies, users, roles,
103
+ access, record policies, shares, cross-tenant grants, service accounts, audit). Your app
104
+ contributes configuration through the seam (`rootIds` from config, an `emailSender`, your
105
+ own object types as `extraObjects`) and its data; access policy is authored by
106
+ administrators as role grants and record policies, not in code. See `CAPABILITIES.md`.
107
+
108
+ Never reimplement session handling, token verification, password hashing, or an
109
+ allow/deny check in app code. Never take a tenant, company, role or user id for an access
110
+ decision from the request body — the framework takes them from the session. If the auth
111
+ or identity surface lacks something you need, that's Rule 4 (flag the gap).
101
112
 
102
113
  ## 7. Fail closed, always
103
114
 
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Astram
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/index.js CHANGED
@@ -18,7 +18,6 @@ const FRAMEWORK_RULES_PATH = join(__dirname, 'FRAMEWORK_RULES.md');
18
18
  const CAPABILITIES_PATH = join(__dirname, 'CAPABILITIES.md');
19
19
 
20
20
  /** The guide version (== the framework release it ships with). */
21
- // eslint-disable-next-line @typescript-eslint/no-var-requires
22
21
  const { version } = require('./package.json');
23
22
 
24
23
  module.exports = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astram/guide",
3
- "version": "0.1.0",
3
+ "version": "0.2.1",
4
4
  "license": "MIT",
5
5
  "description": "Astram framework — the AI/dev GUIDE layer (ADR-0025). Ships the framework's hard rules + capabilities manifest as installed, upgrade-refreshed docs. A consumer app's CLAUDE.md references FRAMEWORK_RULES.md from this package; `npm update` replaces it wholesale so new rules arrive cleanly, while the app's own .astram/guide/ layer is never touched. This is the framework-owned FLOOR: an app rule may tighten it, never weaken it.",
6
6
  "type": "commonjs",
@@ -20,4 +20,4 @@
20
20
  "publishConfig": {
21
21
  "access": "public"
22
22
  }
23
- }
23
+ }