Skip to content
Merged
33 changes: 33 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,44 @@ All notable changes to `sustech-cli` are documented in this file.

## [Unreleased]

### Added

- Added public `talks list` and `talks search` commands for official SUSTech
homepage lectures, showing upcoming events by default and all currently
displayed events with `--all`, with source metadata and text/JSON/JSONL output.

- Added normalized lecture/lab selection bundles with explicit component,
credit-bearing, mutation-ID, task-RWH, and read-back contracts.
- Added bounded `tis selection reconcile` reads for uncertain enrollment,
cart, drop, and bid outcomes.

### Changed

- Made planning-oriented availability, enrollment, degree-progress, and
degree-missing JSON use documented minimum-data projections; grade-free
output is the default.

- Refined `context` into a Shanghai-time daily snapshot with structured current,
next and today's classes, teaching-week parity and makeup details, explicit
empty/unavailable sources, and weather/AQI at normal detail level. Environmental
observations retain source timestamps and freshness; public source failures
no longer prevent a partial snapshot. Non-today live snapshots are rejected.

### Fixed

- Corrected terminal-table alignment for Unicode Roman numerals, ellipses,
and combining characters, and kept grapheme clusters intact during truncation.
- Made `auth status` use a metadata-only macOS Keychain lookup instead of
reading the stored password, and bounded credential-helper subprocesses to
five seconds with a structured `CREDENTIAL_STORE_TIMEOUT` status.
- Linux Secret Service writes now require an immediate verified read-back and
report actionable locked-collection, D-Bus-session, and access-denied states.

### Security

- Selection transport ambiguity now returns an explicit non-retriable outcome
with a local correlation ID, while raw upstream mutation and personal
selection envelopes are excluded from default CLI output.

## [0.10.0] - 2026-08-29

Expand Down
83 changes: 74 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ Public data does not require an account:
sustech calendar day 2026-09-01
sustech faculty search "computer vision"
sustech online talks list --limit 10
sustech talks list
sustech online contact search "教学"
sustech transit lines
sustech library search "graph neural networks" --limit 5
Expand Down Expand Up @@ -166,6 +167,32 @@ Local state can also change through credential login/logout, persistent
and do not overwrite an existing target unless the command explicitly permits
and requests it.

## Official campus lectures

Official university lectures are available without login:

```bash
sustech talks list
sustech talks list --all
sustech talks list --json --pretty
sustech talks search "物理" --jsonl
sustech talks search "物理" --all
```

These commands read the lecture section of the official
[homepage events page](https://www.sustech.edu.cn/zh/home-events.html), excluding
notices. The default view shows lectures whose Beijing start time is still in
the future, ordered from nearest to furthest. Records with an unparseable time
remain visible under “time to confirm” rather than being silently omitted.
`--all` adds lectures whose advertised start time has passed; it means all
lectures currently displayed on the homepage, not the complete historical
archive. Search matches titles, speakers, venues, and time text, and follows the
same upcoming-by-default behavior. Results include title, speaker, venue,
original time text, a normalized Beijing start time when parseable, timing
classification, detail URL, reference time, and official source/fetch metadata.
The existing `online talks` commands continue to use the community-maintained
SUSTech Online source. The new official commands are CLI-only at present.

## Output contract

```bash
Expand Down Expand Up @@ -210,7 +237,10 @@ in the operating system's native credential store:
The password is entered through a hidden prompt, is never accepted as a normal
command-line argument, and is never written to the CLI config. If no safe
backend is available, the CLI returns `CREDENTIAL_STORE_UNAVAILABLE` instead of
falling back to plaintext.
falling back to plaintext. Linux writes are verified by immediate read-back;
locked collections and broken desktop D-Bus sessions produce distinct safe
remediation in `auth status --json` instead of being reported as an expired
password.

On macOS, `auth status` checks Keychain item metadata without reading the
password. Credential-helper commands are bounded to five seconds and report
Expand Down Expand Up @@ -261,6 +291,19 @@ sustech tis enroll apply \
--course-id TIS_INTERNAL_ID --rwh TASK_ID --round bxxk --bid 2 --confirm
```

Availability JSON groups lecture/lab rows into credit-deduplicated bundles and
labels the exact `courseId` (`p_id`) and component `rwh` roles. If apply returns
`TIS_SELECTION_OUTCOME_UNKNOWN`, preserve that exact pair and reconcile without
repeating the write:

```bash
sustech tis selection reconcile enroll \
--course-id TIS_INTERNAL_ID --rwh TASK_ID --round bxxk --attempts 3 --json
```

See [docs/SELECTION_CONTRACTS.md](docs/SELECTION_CONTRACTS.md) for bundle,
identifier, bounded reconciliation, and grade-free planning-output contracts.

Blackboard attachment and submission example:

```bash
Expand Down Expand Up @@ -301,23 +344,45 @@ academic state once, compares it against the existing local state file when
present, reports the changes, and updates that local file. It does not poll, it
does not loop in the background, and it does not write any remote campus state.

## Context v2
## Daily context for AI assistants

```bash
sustech context --level terse
sustech context --live --level normal
sustech context --live --json
sustech context --live --level verbose
```

`context` now has three explicit detail levels:

- `terse`: compact calendar and near-term summary
- `normal`: adds the next deadline, next evaluation, and next exam when known
- `verbose`: adds public weather, AQI, and library-status observations when
`--live` is enabled

`--live` keeps source status explicit. Missing or failed live sources stay
marked as missing or partial; they are not silently backfilled.
- `terse`: date, teaching week and parity, holiday/makeup timetable, and current/next class; only the timetable is requested with `--live`
- `normal` (default): adds the next assignment deadline, evaluation, exam, weather and AQI with `--live`
- `verbose`: also retrieves library opening status

All dates and display times use **Asia/Shanghai**, including on overseas machines.
JSON includes `generatedAt` (snapshot creation), `referenceAt` (the instant used
for class/deadline selection), `timezone`, and the full public `academicDay`.
`schedule.currentClass`, `nextClass`, and `todayClasses` expose ISO timestamps,
periods, locations when available, and `makeupFor` dates. Current and next classes
can appear together; holiday/makeup dates use the same rules as ICS exports.

`sourceStatus` distinguishes a successful empty result (`empty`) from unavailable
data (`missing`). `liveSources` adds errors, missing credentials, partial coverage,
and intentionally skipped requests (`not-requested`). An empty result describes
only the successfully retrieved sources; it is not a claim about all university
systems. A failed public calendar fetch does not prevent other available sources
from being returned.

Weather and air quality include source URLs and upstream `observedAt` timestamps
when supplied. Observations older than three hours are labeled `stale`; absent
timestamps are `unknown`. AQI uses **US EPA** categories, not China's AQI scale.
Public environmental requests time out after eight seconds, and TIS, Blackboard,
and environmental reads run concurrently.

Without `--live`, only the date/calendar snapshot is requested. Use
`context --date YYYY-MM-DD` for a calendar preview (reference time: noon in
Shanghai); combining a non-today date with `--live` is rejected so today's
observations cannot be mistaken for historical data or forecasts.

## Library catalog

Expand Down
5 changes: 5 additions & 0 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,11 @@ commands for them.
- Mutation commands use explicit preview/build phases and post-action
verification. Any ambiguous remote result returns exit code 5 plus
`DO_NOT_RETRY_AUTOMATICALLY` when write state cannot be determined safely.
- TIS selection previews carry a local correlation ID but never claim upstream
idempotency. Transport ambiguity is reconciled through bounded exact
`{courseId, rwh, round}` reads rather than by repeating a mutation.
- Planning-facing TIS output passes through field-allowlisted projections;
broad upstream rows and raw mutation responses remain outside CLI JSON.
- Consequence metadata lives in `src/core/consequences.ts` so agents can inspect
risks and follow-up checks without scraping prose.
- New authenticated campus-service wrappers are validated with protocol fixtures
Expand Down
7 changes: 7 additions & 0 deletions docs/AUTHENTICATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,13 @@ retried automatically. If a helper exceeds that deadline, structured status
sets `reasonCode` to `CREDENTIAL_STORE_TIMEOUT`, marks the backend unavailable
for that probe, and leaves the credential and profile metadata unchanged.

Credential writes are verified by an immediate read-back before profile
metadata is committed. Linux errors distinguish a locked collection, a missing
desktop D-Bus/Secret Service session, an access denial, and an unclassified
`secret-tool` failure. Run `sustech auth status --json` in the same unlocked
graphical session and follow its `remediation`; do not delete profile metadata
or assume the password expired merely because the collection is locked.

## Profiles

The default profile is named `default`. Multiple accounts use explicit names:
Expand Down
6 changes: 6 additions & 0 deletions docs/OUTPUT.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,12 @@ Credential commands return backend, profile, availability, and masked account
metadata only. Passwords, cookies, bearer tokens, and keyring values are never
part of text, JSON, JSONL, error details, or capability output.

Planning-oriented TIS commands additionally use field allowlists. Available
courses are emitted as normalized bundles, enrolled rows omit broad description
fields, and `tis degree missing` omits letter grades and numeric scores. Course
grades in `tis degree progress` require the explicit `--details` option. See
`SELECTION_CONTRACTS.md` for exact bundle, identifier, and projection semantics.

Blackboard attachment listings likewise omit signed `bbcswebdav` URLs. A
successful `bb download` result contains only stable attachment metadata, the
absolute destination path, byte count, content type, and SHA-256.
60 changes: 60 additions & 0 deletions docs/SELECTION_CONTRACTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# TIS selection contracts

The selection surface separates catalog rows, selectable bundles, mutation identifiers, and read-back identifiers. Consumers must not infer one identifier's meaning from its spelling.

## Bundled availability

`sustech tis courses available ... --json` returns `data.bundles`. A bundle contains:

- `bundleId`: an explicit source bundle ID when TIS exposes one; otherwise a stable selection/task-scoped identity.
- `components`: lecture, lab, tutorial, other, or unknown rows. Every component states whether it is required and identifies its task `rwh`.
- `credits` and `creditStatus`: equal repeated component credits are counted once. Conflicting component credits produce `creditStatus: "ambiguous"` and omit `credits` instead of guessing or summing.
- `teachingTeam` and `meetings`: unions across all components, retaining parity-week schedules.
- `operationTargets`: exact component-level mutation `courseId`, task `rwh`, payload field, and read-back identity.
- `selectableWithoutGuessing`: true only when every required component has the explicit identifier pair needed for mutation and verification, and source course identity and credit evidence do not conflict.

Duplicate source rows for the same component are merged and reported in `warnings`. Default CLI output never contains the upstream selection envelope, enrolled/cart raw rows, credentials, cookies, tokens, or unrelated student fields. `retainCourseSourceRecord` is a library-level diagnostics-only escape hatch and is not called by CLI commands.

## Identifier meanings

| Name | Meaning | Accepted by |
| --- | --- | --- |
| `bundleId` | normalized course bundle identity | display, planning, grouping only |
| `componentId` / `taskId` / `rwh` | exact teaching-task component | required together with `courseId` for apply and reconciliation read-back |
| `courseId` | opaque selection mutation identifier | CLI `--course-id`; serialized as upstream `p_id` |
| `clientRequestId` | local correlation identifier | output/errors only; it is not an upstream idempotency key |

Do not pass `bundleId`, course code, or `rwh` as `--course-id`. Do not treat `courseId` alone as a unique lecture/lab component: exact verification keys on `{courseId, rwh}`.

## Uncertain writes and reconciliation

TIS does not currently expose a verified idempotency-key facility for these endpoints. Every preview therefore says `upstreamKeySupported: false` and `automaticRetry: "forbidden"`. The generated `clientRequestId` is not added to the upstream payload.

If a request is known to fail before submission, the CLI returns `TIS_SELECTION_NOT_SUBMITTED`, exit 4, and `NO_MUTATION_PERFORMED`. If submission may have started but no conclusive response arrives, it returns `TIS_SELECTION_OUTCOME_UNKNOWN`, exit 5, and `DO_NOT_RETRY_AUTOMATICALLY`.

Use the exact target from the error:

```bash
sustech tis selection reconcile cart.add \
--course-id SELECTION_ID --rwh TASK_ID --round bxxk \
--attempts 3 --json
```

Reconciliation performs two to five bounded read-only queries and reports:

- `applied`: the final bounded observation reached the requested exact state;
- `not_applied`: at least two consistent exact observations retained the inverse state and no desired/conflicting observation appeared;
- `still_uncertain`: a query failed, identifiers conflicted, round metadata was missing or mismatched, observations regressed, or evidence remained incomplete (including a missing bid value).

None of these states authorizes an automatic mutation retry. `not_applied` means a human or higher-level workflow may review a new preview; it does not reuse the uncertain request.

## Privacy-minimized planning projections

Planning commands use documented allowlists:

- `tis courses available`: normalized bundles, components, exact identifiers, teaching teams, meetings, capacity/credit context, and report time;
- `tis enrolled`: course identity, exact `rwh`, teaching team, and meeting coordinates;
- `tis degree progress`: summary/category/module data by default; course grades appear only with explicit `--details`;
- `tis degree missing`: completion classification may guide gap reasoning, but letter grades and numeric scores are removed from both text and JSON.

The projection guard rejects credential, cookie, token, raw-envelope, SID, and unrelated student-identifier keys before output.
Loading